Compare commits

..
Author SHA1 Message Date
Muhsin Keloth e622375b23 chore: more cleanups 2024-06-11 13:02:02 +05:30
Muhsin Keloth 410eeeb610 chore: code cleanup 2024-06-11 12:45:59 +05:30
Muhsin Keloth 343f7ed2d3 feat: cleanup the fcm body part 2024-06-05 13:13:21 +05:30
Muhsin Keloth 61eaa96b85 feat: add fcm service 2024-06-05 11:09:22 +05:30
134 changed files with 866 additions and 3567 deletions
-2
View File
@@ -85,8 +85,6 @@ SMTP_OPENSSL_VERIFY_MODE=peer
# Comment out the following environment variables if required by your SMTP server
# SMTP_TLS=
# SMTP_SSL=
# SMTP_OPEN_TIMEOUT
# SMTP_READ_TIMEOUT
# Mail Incoming
# This is the domain set for the reply emails when conversation continuity is enabled
+2 -1
View File
@@ -75,4 +75,5 @@ yalc.lock
yarn-debug.log*
.yarn-integrity
/storybook-static
/storybook-static
config/chatwoot-fcm-service-account-file.json
+6 -6
View File
@@ -4,7 +4,7 @@ ruby '3.2.2'
##-- base gems for rails --##
gem 'rack-cors', '2.0.0', require: 'rack/cors'
gem 'rails', '~> 7.0.8.4'
gem 'rails', '~> 7.0.8.1'
# Reduces boot times through caching; required in config/boot.rb
gem 'bootsnap', require: false
@@ -61,7 +61,7 @@ gem 'redis-namespace'
gem 'activerecord-import'
##--- gems for server & infra configuration ---##
gem 'dotenv-rails', '>= 3.0.0'
gem 'dotenv-rails'
gem 'foreman'
gem 'puma'
gem 'webpacker'
@@ -111,9 +111,9 @@ gem 'elastic-apm', require: false
gem 'newrelic_rpm', require: false
gem 'newrelic-sidekiq-metrics', '>= 1.6.2', require: false
gem 'scout_apm', require: false
gem 'sentry-rails', '>= 5.18.0', require: false
gem 'sentry-rails', '>= 5.14.0', require: false
gem 'sentry-ruby', require: false
gem 'sentry-sidekiq', '>= 5.18.0', require: false
gem 'sentry-sidekiq', '>= 5.15.0', require: false
##-- background job processing --##
gem 'sidekiq', '>= 7.2.4'
@@ -122,7 +122,7 @@ gem 'sidekiq-cron', '>= 1.12.0'
##-- Push notification service --##
gem 'fcm'
gem 'web-push', '>= 3.0.1'
gem 'web-push'
##-- geocoding / parse location from ip --##
# http://www.rubygeocoder.com/
@@ -228,7 +228,7 @@ group :development, :test do
gem 'mock_redis'
gem 'pry-rails'
gem 'rspec_junit_formatter'
gem 'rspec-rails', '>= 6.1.3'
gem 'rspec-rails'
gem 'rubocop', require: false
gem 'rubocop-performance', require: false
gem 'rubocop-rails', require: false
+99 -97
View File
@@ -33,70 +33,70 @@ GIT
GEM
remote: https://rubygems.org/
specs:
actioncable (7.0.8.4)
actionpack (= 7.0.8.4)
activesupport (= 7.0.8.4)
actioncable (7.0.8.3)
actionpack (= 7.0.8.3)
activesupport (= 7.0.8.3)
nio4r (~> 2.0)
websocket-driver (>= 0.6.1)
actionmailbox (7.0.8.4)
actionpack (= 7.0.8.4)
activejob (= 7.0.8.4)
activerecord (= 7.0.8.4)
activestorage (= 7.0.8.4)
activesupport (= 7.0.8.4)
actionmailbox (7.0.8.3)
actionpack (= 7.0.8.3)
activejob (= 7.0.8.3)
activerecord (= 7.0.8.3)
activestorage (= 7.0.8.3)
activesupport (= 7.0.8.3)
mail (>= 2.7.1)
net-imap
net-pop
net-smtp
actionmailer (7.0.8.4)
actionpack (= 7.0.8.4)
actionview (= 7.0.8.4)
activejob (= 7.0.8.4)
activesupport (= 7.0.8.4)
actionmailer (7.0.8.3)
actionpack (= 7.0.8.3)
actionview (= 7.0.8.3)
activejob (= 7.0.8.3)
activesupport (= 7.0.8.3)
mail (~> 2.5, >= 2.5.4)
net-imap
net-pop
net-smtp
rails-dom-testing (~> 2.0)
actionpack (7.0.8.4)
actionview (= 7.0.8.4)
activesupport (= 7.0.8.4)
actionpack (7.0.8.3)
actionview (= 7.0.8.3)
activesupport (= 7.0.8.3)
rack (~> 2.0, >= 2.2.4)
rack-test (>= 0.6.3)
rails-dom-testing (~> 2.0)
rails-html-sanitizer (~> 1.0, >= 1.2.0)
actiontext (7.0.8.4)
actionpack (= 7.0.8.4)
activerecord (= 7.0.8.4)
activestorage (= 7.0.8.4)
activesupport (= 7.0.8.4)
actiontext (7.0.8.3)
actionpack (= 7.0.8.3)
activerecord (= 7.0.8.3)
activestorage (= 7.0.8.3)
activesupport (= 7.0.8.3)
globalid (>= 0.6.0)
nokogiri (>= 1.8.5)
actionview (7.0.8.4)
activesupport (= 7.0.8.4)
actionview (7.0.8.3)
activesupport (= 7.0.8.3)
builder (~> 3.1)
erubi (~> 1.4)
rails-dom-testing (~> 2.0)
rails-html-sanitizer (~> 1.1, >= 1.2.0)
active_record_query_trace (1.8)
activejob (7.0.8.4)
activesupport (= 7.0.8.4)
activejob (7.0.8.3)
activesupport (= 7.0.8.3)
globalid (>= 0.3.6)
activemodel (7.0.8.4)
activesupport (= 7.0.8.4)
activerecord (7.0.8.4)
activemodel (= 7.0.8.4)
activesupport (= 7.0.8.4)
activemodel (7.0.8.3)
activesupport (= 7.0.8.3)
activerecord (7.0.8.3)
activemodel (= 7.0.8.3)
activesupport (= 7.0.8.3)
activerecord-import (1.4.1)
activerecord (>= 4.2)
activestorage (7.0.8.4)
actionpack (= 7.0.8.4)
activejob (= 7.0.8.4)
activerecord (= 7.0.8.4)
activesupport (= 7.0.8.4)
activestorage (7.0.8.3)
actionpack (= 7.0.8.3)
activejob (= 7.0.8.3)
activerecord (= 7.0.8.3)
activesupport (= 7.0.8.3)
marcel (~> 1.0)
mini_mime (>= 1.1.0)
activesupport (7.0.8.4)
activesupport (7.0.8.3)
concurrent-ruby (~> 1.0, >= 1.0.2)
i18n (>= 1.6, < 2)
minitest (>= 5.1)
@@ -150,13 +150,13 @@ GEM
statsd-ruby (~> 1.1)
base64 (0.2.0)
bcrypt (3.1.20)
bigdecimal (3.1.8)
bigdecimal (3.1.7)
bindex (0.8.1)
bootsnap (1.16.0)
msgpack (~> 1.2)
brakeman (5.4.1)
browser (5.3.1)
builder (3.3.0)
builder (3.2.4)
bullet (7.0.7)
activesupport (>= 3.0.0)
uniform_notifier (~> 1.11)
@@ -169,7 +169,7 @@ GEM
climate_control (1.2.0)
coderay (1.1.3)
commonmarker (0.23.10)
concurrent-ruby (1.3.3)
concurrent-ruby (1.2.3)
connection_pool (2.4.1)
crack (0.4.5)
rexml
@@ -204,16 +204,16 @@ GEM
bcrypt (~> 3.0)
devise (> 3.5.2, < 5)
rails (>= 4.2.0, < 7.2)
diff-lcs (1.5.1)
diff-lcs (1.5.0)
digest-crc (0.6.4)
rake (>= 12.0.0, < 14.0.0)
docile (1.4.0)
domain_name (0.5.20190701)
unf (>= 0.0.5, < 1.0.0)
dotenv (3.1.2)
dotenv-rails (3.1.2)
dotenv (= 3.1.2)
railties (>= 6.1)
dotenv (2.8.1)
dotenv-rails (2.8.1)
dotenv (= 2.8.1)
railties (>= 3.2)
down (5.4.0)
addressable (~> 2.8)
ecma-re-validator (0.4.0)
@@ -223,7 +223,7 @@ GEM
http (>= 3.0)
ruby2_keywords
email_reply_trimmer (0.1.13)
erubi (1.13.0)
erubi (1.12.0)
et-orbi (1.2.7)
tzinfo
execjs (2.8.1)
@@ -355,6 +355,7 @@ GEM
hana (1.3.7)
hashdiff (1.0.1)
hashie (5.0.0)
hkdf (1.0.0)
http (5.1.1)
addressable (~> 2.8)
http-cookie (~> 1.0)
@@ -459,8 +460,8 @@ GEM
mime-types-data (3.2023.0218.1)
mini_magick (4.12.0)
mini_mime (1.1.5)
mini_portile2 (2.8.7)
minitest (5.24.0)
mini_portile2 (2.8.6)
minitest (5.23.0)
mock_redis (0.36.0)
ruby2_keywords
msgpack (1.7.0)
@@ -473,7 +474,7 @@ GEM
uri
net-http-persistent (4.0.2)
connection_pool (~> 2.2)
net-imap (0.4.12)
net-imap (0.4.11)
date
net-protocol
net-pop (0.1.2)
@@ -489,14 +490,14 @@ GEM
newrelic_rpm (9.6.0)
base64
nio4r (2.7.3)
nokogiri (1.16.6)
nokogiri (1.16.5)
mini_portile2 (~> 2.8.2)
racc (~> 1.4)
nokogiri (1.16.6-arm64-darwin)
nokogiri (1.16.5-arm64-darwin)
racc (~> 1.4)
nokogiri (1.16.6-x86_64-darwin)
nokogiri (1.16.5-x86_64-darwin)
racc (~> 1.4)
nokogiri (1.16.6-x86_64-linux)
nokogiri (1.16.5-x86_64-linux)
racc (~> 1.4)
oauth (1.1.0)
oauth-tty (~> 1.0, >= 1.0.1)
@@ -526,7 +527,7 @@ GEM
omniauth-rails_csrf_protection (1.0.2)
actionpack (>= 4.2)
omniauth (~> 2.0)
openssl (3.2.0)
openssl (3.1.0)
orm_adapter (0.5.0)
os (1.1.4)
parallel (1.23.0)
@@ -550,7 +551,7 @@ GEM
pundit (2.3.0)
activesupport (>= 3.0.0)
raabro (1.4.0)
racc (1.8.0)
racc (1.7.3)
rack (2.2.9)
rack-attack (6.7.0)
rack (>= 1.0, < 4)
@@ -568,20 +569,20 @@ GEM
rack-test (2.1.0)
rack (>= 1.3)
rack-timeout (0.6.3)
rails (7.0.8.4)
actioncable (= 7.0.8.4)
actionmailbox (= 7.0.8.4)
actionmailer (= 7.0.8.4)
actionpack (= 7.0.8.4)
actiontext (= 7.0.8.4)
actionview (= 7.0.8.4)
activejob (= 7.0.8.4)
activemodel (= 7.0.8.4)
activerecord (= 7.0.8.4)
activestorage (= 7.0.8.4)
activesupport (= 7.0.8.4)
rails (7.0.8.3)
actioncable (= 7.0.8.3)
actionmailbox (= 7.0.8.3)
actionmailer (= 7.0.8.3)
actionpack (= 7.0.8.3)
actiontext (= 7.0.8.3)
actionview (= 7.0.8.3)
activejob (= 7.0.8.3)
activemodel (= 7.0.8.3)
activerecord (= 7.0.8.3)
activestorage (= 7.0.8.3)
activesupport (= 7.0.8.3)
bundler (>= 1.15.0)
railties (= 7.0.8.4)
railties (= 7.0.8.3)
rails-dom-testing (2.2.0)
activesupport (>= 5.0.0)
minitest
@@ -589,9 +590,9 @@ GEM
rails-html-sanitizer (1.6.0)
loofah (~> 2.21)
nokogiri (~> 1.14)
railties (7.0.8.4)
actionpack (= 7.0.8.4)
activesupport (= 7.0.8.4)
railties (7.0.8.3)
actionpack (= 7.0.8.3)
activesupport (= 7.0.8.3)
method_source
rake (>= 12.2)
thor (~> 1.0)
@@ -603,7 +604,7 @@ GEM
ffi (~> 1.0)
redis (5.0.6)
redis-client (>= 0.9.0)
redis-client (0.22.2)
redis-client (0.22.1)
connection_pool
redis-namespace (1.10.0)
redis (>= 4)
@@ -629,23 +630,23 @@ GEM
nokogiri
rexml (3.2.8)
strscan (>= 3.0.9)
rspec-core (3.13.0)
rspec-support (~> 3.13.0)
rspec-expectations (3.13.1)
rspec-core (3.12.2)
rspec-support (~> 3.12.0)
rspec-expectations (3.12.3)
diff-lcs (>= 1.2.0, < 2.0)
rspec-support (~> 3.13.0)
rspec-mocks (3.13.1)
rspec-support (~> 3.12.0)
rspec-mocks (3.12.5)
diff-lcs (>= 1.2.0, < 2.0)
rspec-support (~> 3.13.0)
rspec-rails (6.1.3)
rspec-support (~> 3.12.0)
rspec-rails (6.0.2)
actionpack (>= 6.1)
activesupport (>= 6.1)
railties (>= 6.1)
rspec-core (~> 3.13)
rspec-expectations (~> 3.13)
rspec-mocks (~> 3.13)
rspec-support (~> 3.13)
rspec-support (3.13.1)
rspec-core (~> 3.12)
rspec-expectations (~> 3.12)
rspec-mocks (~> 3.12)
rspec-support (~> 3.12)
rspec-support (3.12.0)
rspec_junit_formatter (0.6.0)
rspec-core (>= 2, < 4, != 2.12.0)
rubocop (1.50.2)
@@ -703,14 +704,14 @@ GEM
activesupport (>= 4)
selectize-rails (0.12.6)
semantic_range (3.0.0)
sentry-rails (5.18.0)
sentry-rails (5.17.3)
railties (>= 5.0)
sentry-ruby (~> 5.18.0)
sentry-ruby (5.18.0)
sentry-ruby (~> 5.17.3)
sentry-ruby (5.17.3)
bigdecimal
concurrent-ruby (~> 1.0, >= 1.0.2)
sentry-sidekiq (5.18.0)
sentry-ruby (~> 5.18.0)
sentry-sidekiq (5.17.3)
sentry-ruby (~> 5.17.3)
sidekiq (>= 3.0)
sexp_processor (4.17.0)
shoulda-matchers (5.3.0)
@@ -799,7 +800,8 @@ GEM
activemodel (>= 6.0.0)
bindex (>= 0.4.0)
railties (>= 6.0.0)
web-push (3.0.1)
web-push (3.0.0)
hkdf (~> 1.0)
jwt (~> 2.0)
openssl (~> 3.0)
webmock (3.18.1)
@@ -819,7 +821,7 @@ GEM
working_hours (1.4.1)
activesupport (>= 3.2)
tzinfo
zeitwerk (2.6.16)
zeitwerk (2.6.14)
PLATFORMS
arm64-darwin-20
@@ -861,7 +863,7 @@ DEPENDENCIES
devise (>= 4.9.4)
devise-secure_password!
devise_token_auth (>= 1.2.3)
dotenv-rails (>= 3.0.0)
dotenv-rails
down
elastic-apm
email_reply_trimmer
@@ -916,13 +918,13 @@ DEPENDENCIES
rack-cors (= 2.0.0)
rack-mini-profiler (>= 3.2.0)
rack-timeout
rails (~> 7.0.8.4)
rails (~> 7.0.8.1)
redis
redis-namespace
responders (>= 3.1.1)
rest-client
reverse_markdown
rspec-rails (>= 6.1.3)
rspec-rails
rspec_junit_formatter
rubocop
rubocop-performance
@@ -931,9 +933,9 @@ DEPENDENCIES
scout_apm
scss_lint
seed_dump
sentry-rails (>= 5.18.0)
sentry-rails (>= 5.14.0)
sentry-ruby
sentry-sidekiq (>= 5.18.0)
sentry-sidekiq (>= 5.15.0)
shoulda-matchers
sidekiq (>= 7.2.4)
sidekiq-cron (>= 1.12.0)
@@ -953,7 +955,7 @@ DEPENDENCIES
uglifier
valid_email2
web-console (>= 4.2.1)
web-push (>= 3.0.1)
web-push
webmock
webpacker
wisper (= 2.0.0)
+1 -1
View File
@@ -1 +1 @@
3.9.0
3.3.1
+1 -1
View File
@@ -1 +1 @@
2.8.0
2.7.0
@@ -1,32 +0,0 @@
class Api::V1::Accounts::Google::AuthorizationsController < Api::V1::Accounts::BaseController
include GoogleConcern
before_action :check_authorization
def create
email = params[:authorization][:email]
redirect_url = google_client.auth_code.authorize_url(
{
redirect_uri: "#{base_url}/google/callback",
scope: 'email profile https://mail.google.com/',
response_type: 'code',
prompt: 'consent', # the oauth flow does not return a refresh token, this is supposed to fix it
access_type: 'offline', # the default is 'online'
client_id: GlobalConfigService.load('GOOGLE_OAUTH_CLIENT_ID', nil)
}
)
if redirect_url
cache_key = "google::#{email.downcase}"
::Redis::Alfred.setex(cache_key, Current.account.id, 5.minutes)
render json: { success: true, url: redirect_url }
else
render json: { success: false }, status: :unprocessable_entity
end
end
private
def check_authorization
raise Pundit::NotAuthorizedError unless Current.account_user.administrator?
end
end
@@ -12,8 +12,8 @@ class Api::V1::Accounts::Microsoft::AuthorizationsController < Api::V1::Accounts
}
)
if redirect_url
cache_key = "microsoft::#{email.downcase}"
::Redis::Alfred.setex(cache_key, Current.account.id, 5.minutes)
email = email.downcase
::Redis::Alfred.setex(email, Current.account.id, 5.minutes)
render json: { success: true, url: redirect_url }
else
render json: { success: false }, status: :unprocessable_entity
@@ -1,20 +0,0 @@
module GoogleConcern
extend ActiveSupport::Concern
def google_client
app_id = GlobalConfigService.load('GOOGLE_OAUTH_CLIENT_ID', nil)
app_secret = GlobalConfigService.load('GOOGLE_OAUTH_CLIENT_SECRET', nil)
::OAuth2::Client.new(app_id, app_secret, {
site: 'https://oauth2.googleapis.com',
authorize_url: 'https://accounts.google.com/o/oauth2/auth',
token_url: 'https://accounts.google.com/o/oauth2/token'
})
end
private
def base_url
ENV.fetch('FRONTEND_URL', 'http://localhost:3000')
end
end
@@ -15,6 +15,10 @@ module MicrosoftConcern
private
def parsed_body
@parsed_body ||= Rack::Utils.parse_nested_query(@response.raw_response.body)
end
def base_url
ENV.fetch('FRONTEND_URL', 'http://localhost:3000')
end
+1 -11
View File
@@ -37,7 +37,7 @@ class DashboardController < ActionController::Base
end
def set_dashboard_scripts
@dashboard_scripts = sensitive_path? ? nil : GlobalConfig.get_value('DASHBOARD_SCRIPTS')
@dashboard_scripts = GlobalConfig.get_value('DASHBOARD_SCRIPTS')
end
def ensure_installation_onboarding
@@ -75,14 +75,4 @@ class DashboardController < ActionController::Base
'application'
end
end
def sensitive_path?
# dont load dashboard scripts on sensitive paths like password reset
sensitive_paths = [edit_user_password_path].freeze
# remove app prefix
current_path = request.path.gsub(%r{^/app}, '')
sensitive_paths.include?(current_path)
end
end
@@ -1,18 +0,0 @@
class Google::CallbacksController < OauthCallbackController
include GoogleConcern
private
def provider_name
'google'
end
def imap_address
'imap.gmail.com'
end
def oauth_client
# from GoogleConcern
google_client
end
end
@@ -1,17 +1,90 @@
class Microsoft::CallbacksController < OauthCallbackController
class Microsoft::CallbacksController < ApplicationController
include MicrosoftConcern
def show
@response = microsoft_client.auth_code.get_token(
oauth_code,
redirect_uri: "#{base_url}/microsoft/callback"
)
inbox, already_exists = find_or_create_inbox
::Redis::Alfred.delete(users_data['email'].downcase)
if already_exists
redirect_to app_microsoft_inbox_settings_url(account_id: account.id, inbox_id: inbox.id)
else
redirect_to app_microsoft_inbox_agents_url(account_id: account.id, inbox_id: inbox.id)
end
rescue StandardError => e
ChatwootExceptionTracker.new(e).capture_exception
redirect_to '/'
end
private
def oauth_client
microsoft_client
def oauth_code
params[:code]
end
def provider_name
'microsoft'
def users_data
decoded_token = JWT.decode parsed_body[:id_token], nil, false
decoded_token[0]
end
def imap_address
'outlook.office365.com'
def parsed_body
@parsed_body ||= @response.response.parsed
end
def account_id
::Redis::Alfred.get(users_data['email'].downcase)
end
def account
@account ||= Account.find(account_id)
end
def find_or_create_inbox
channel_email = Channel::Email.find_by(email: users_data['email'], account: account)
# we need this value to know where to redirect on sucessful processing of the callback
channel_exists = channel_email.present?
channel_email ||= create_microsoft_channel_with_inbox
update_microsoft_channel(channel_email)
# reauthorize channel, this code path only triggers when microsoft auth is successful
# reauthorized will also update cache keys for the associated inbox
channel_email.reauthorized!
[channel_email.inbox, channel_exists]
end
# Fallback name, for when name field is missing from users_data
def fallback_name
users_data['email'].split('@').first.parameterize.titleize
end
def create_microsoft_channel_with_inbox
ActiveRecord::Base.transaction do
channel_email = Channel::Email.create!(email: users_data['email'], account: account)
account.inboxes.create!(
account: account,
channel: channel_email,
name: users_data['name'] || fallback_name
)
channel_email
end
end
def update_microsoft_channel(channel_email)
channel_email.update!({
imap_login: users_data['email'], imap_address: 'outlook.office365.com',
imap_port: '993', imap_enabled: true,
provider: 'microsoft',
provider_config: {
access_token: parsed_body['access_token'],
refresh_token: parsed_body['refresh_token'],
expires_on: (Time.current.utc + 1.hour).to_s
}
})
end
end
@@ -1,108 +0,0 @@
class OauthCallbackController < ApplicationController
def show
@response = oauth_client.auth_code.get_token(
oauth_code,
redirect_uri: "#{base_url}/#{provider_name}/callback"
)
handle_response
::Redis::Alfred.delete(cache_key)
rescue StandardError => e
ChatwootExceptionTracker.new(e).capture_exception
redirect_to '/'
end
private
def handle_response
inbox, already_exists = find_or_create_inbox
if already_exists
redirect_to app_email_inbox_settings_url(account_id: account.id, inbox_id: inbox.id)
else
redirect_to app_email_inbox_agents_url(account_id: account.id, inbox_id: inbox.id)
end
end
def find_or_create_inbox
channel_email = Channel::Email.find_by(email: users_data['email'], account: account)
# we need this value to know where to redirect on sucessful processing of the callback
channel_exists = channel_email.present?
channel_email ||= create_channel_with_inbox
update_channel(channel_email)
# reauthorize channel, this code path only triggers when microsoft auth is successful
# reauthorized will also update cache keys for the associated inbox
channel_email.reauthorized!
[channel_email.inbox, channel_exists]
end
def update_channel(channel_email)
channel_email.update!({
imap_login: users_data['email'], imap_address: imap_address,
imap_port: '993', imap_enabled: true,
provider: provider_name,
provider_config: {
access_token: parsed_body['access_token'],
refresh_token: parsed_body['refresh_token'],
expires_on: (Time.current.utc + 1.hour).to_s
}
})
end
def provider_name
raise NotImplementedError
end
def oauth_client
raise NotImplementedError
end
def cache_key
"#{provider_name}::#{users_data['email'].downcase}"
end
def create_channel_with_inbox
ActiveRecord::Base.transaction do
channel_email = Channel::Email.create!(email: users_data['email'], account: account)
account.inboxes.create!(
account: account,
channel: channel_email,
name: users_data['name'] || fallback_name
)
channel_email
end
end
def users_data
decoded_token = JWT.decode parsed_body[:id_token], nil, false
decoded_token[0]
end
def account_id
::Redis::Alfred.get(cache_key)
end
def account
@account ||= Account.find(account_id)
end
# Fallback name, for when name field is missing from users_data
def fallback_name
users_data['email'].split('@').first.parameterize.titleize
end
def oauth_code
params[:code]
end
def base_url
ENV.fetch('FRONTEND_URL', 'http://localhost:3000')
end
def parsed_body
@parsed_body ||= @response.response.parsed
end
end
@@ -1,14 +0,0 @@
/* global axios */
import ApiClient from '../ApiClient';
class MicrosoftClient extends ApiClient {
constructor() {
super('google', { accountScoped: true });
}
generateAuthorization(payload) {
return axios.post(`${this.url}/authorization`, payload);
}
}
export default new MicrosoftClient();
@@ -1,6 +1,6 @@
<template>
<button
class="bg-white dark:bg-slate-900 cursor-pointer flex flex-col justify-end transition-all duration-200 ease-in -m-px py-4 px-0 items-center border border-solid border-slate-25 dark:border-slate-800 hover:border-woot-500 dark:hover:border-woot-500 hover:shadow-md hover:z-50 disabled:opacity-60"
class="bg-white dark:bg-slate-900 cursor-pointer flex flex-col transition-all duration-200 ease-in -m-px py-4 px-0 items-center border border-solid border-slate-25 dark:border-slate-800 hover:border-woot-500 dark:hover:border-woot-500 hover:shadow-md hover:z-50 disabled:opacity-60"
@click="$emit('click')"
>
<img :src="src" :alt="title" class="w-1/2 my-4 mx-auto" />
@@ -314,28 +314,14 @@ export default {
unassigned: 'unAssignedCount',
all: 'allCount',
};
const userPermissions = this.currentUser.permissions;
const allowedTabs = {
me: ['conversation_manage', 'conversation_participating_manage'].some(
permission => userPermissions.includes(permission)
),
unassigned: [
'conversation_manage',
'conversation_unassigned_manage',
].some(permission => userPermissions.includes(permission)),
all: true,
};
return Object.keys(allowedTabs)
.filter(key => allowedTabs[key])
.map(key => {
const count =
this.conversationStats[ASSIGNEE_TYPE_TAB_KEYS[key]] || 0;
return {
key,
name: this.$t(`CHAT_LIST.ASSIGNEE_TYPE_TABS.${key}`),
count,
};
});
return Object.keys(ASSIGNEE_TYPE_TAB_KEYS).map(key => {
const count = this.conversationStats[ASSIGNEE_TYPE_TAB_KEYS[key]] || 0;
return {
key,
name: this.$t(`CHAT_LIST.ASSIGNEE_TYPE_TABS.${key}`),
count,
};
});
},
showAssigneeInConversationCard() {
return (
@@ -20,7 +20,7 @@
:teams="teams"
:custom-views="customViews"
:menu-config="activeSecondaryMenu"
:current-user="currentUser"
:current-role="currentRole"
:is-on-chatwoot-cloud="isOnChatwootCloud"
@add-label="showAddLabelPopup"
@toggle-accounts="toggleAccountModal"
@@ -98,12 +98,9 @@ export default {
return getSidebarItems(this.accountId);
},
primaryMenuItems() {
const permissions = this.currentUser.permissions;
const menuItems = this.sideMenuConfig.primaryMenu;
return menuItems.filter(menuItem => {
const isAvailableForTheUser = permissions.some(permission =>
menuItem.meta.permissions.includes(permission)
);
const isAvailableForTheUser = menuItem.roles.includes(this.currentRole);
if (!isAvailableForTheUser) {
return false;
@@ -11,9 +11,6 @@ const campaigns = accountId => ({
hasSubMenu: false,
toState: frontendURL(`accounts/${accountId}/campaigns/ongoing`),
toStateName: 'ongoing_campaigns',
meta: {
permissions: ['account_manage'],
},
},
{
key: 'oneOffCampaigns',
@@ -22,9 +19,6 @@ const campaigns = accountId => ({
hasSubMenu: false,
toState: frontendURL(`accounts/${accountId}/campaigns/one_off`),
toStateName: 'one_off',
meta: {
permissions: ['account_manage'],
},
},
],
});
@@ -14,9 +14,6 @@ const contacts = accountId => ({
label: 'ALL_CONTACTS',
hasSubMenu: false,
toState: frontendURL(`accounts/${accountId}/contacts`),
meta: {
permissions: ['account_manage'],
},
toStateName: 'contacts_dashboard',
},
],
@@ -28,9 +28,6 @@ const conversations = accountId => ({
key: 'conversations',
toState: frontendURL(`accounts/${accountId}/dashboard`),
toolTip: 'Conversation from all subscribed inboxes',
meta: {
permissions: ['account_manage'],
},
toStateName: 'home',
},
{
@@ -38,9 +35,6 @@ const conversations = accountId => ({
label: 'MENTIONED_CONVERSATIONS',
key: 'conversation_mentions',
toState: frontendURL(`accounts/${accountId}/mentions/conversations`),
meta: {
permissions: ['account_manage'],
},
toStateName: 'conversation_mentions',
},
{
@@ -48,9 +42,6 @@ const conversations = accountId => ({
label: 'UNATTENDED_CONVERSATIONS',
key: 'conversation_unattended',
toState: frontendURL(`accounts/${accountId}/unattended/conversations`),
meta: {
permissions: ['account_manage'],
},
toStateName: 'conversation_unattended',
},
],
@@ -9,14 +9,7 @@ const primaryMenuItems = accountId => [
featureFlag: FEATURE_FLAGS.INBOX_VIEW,
toState: frontendURL(`accounts/${accountId}/inbox-view`),
toStateName: 'inbox_view',
meta: {
permissions: [
'account_manage',
'conversation_manage',
'conversation_participating_manage',
'conversation_unassigned_manage',
],
},
roles: ['administrator', 'agent'],
},
{
icon: 'chat',
@@ -24,14 +17,7 @@ const primaryMenuItems = accountId => [
label: 'CONVERSATIONS',
toState: frontendURL(`accounts/${accountId}/dashboard`),
toStateName: 'home',
meta: {
permissions: [
'account_manage',
'conversation_manage',
'conversation_participating_manage',
'conversation_unassigned_manage',
],
},
roles: ['administrator', 'agent'],
},
{
icon: 'book-contacts',
@@ -40,14 +26,7 @@ const primaryMenuItems = accountId => [
featureFlag: FEATURE_FLAGS.CRM,
toState: frontendURL(`accounts/${accountId}/contacts`),
toStateName: 'contacts_dashboard',
meta: {
permissions: [
'account_manage',
'conversation_manage',
'conversation_participating_manage',
'conversation_unassigned_manage',
],
},
roles: ['administrator', 'agent'],
},
{
icon: 'arrow-trending-lines',
@@ -56,9 +35,7 @@ const primaryMenuItems = accountId => [
featureFlag: FEATURE_FLAGS.REPORTS,
toState: frontendURL(`accounts/${accountId}/reports`),
toStateName: 'settings_account_reports',
meta: {
permissions: ['account_manage'],
},
roles: ['administrator'],
},
{
icon: 'megaphone',
@@ -67,9 +44,7 @@ const primaryMenuItems = accountId => [
featureFlag: FEATURE_FLAGS.CAMPAIGNS,
toState: frontendURL(`accounts/${accountId}/campaigns`),
toStateName: 'ongoing_campaigns',
meta: {
permissions: ['account_manage'],
},
roles: ['administrator'],
},
{
icon: 'library',
@@ -79,9 +54,7 @@ const primaryMenuItems = accountId => [
alwaysVisibleOnChatwootInstances: true,
toState: frontendURL(`accounts/${accountId}/portals`),
toStateName: 'default_portal_articles',
meta: {
permissions: ['account_manage'],
},
roles: ['administrator'],
},
{
icon: 'settings',
@@ -89,14 +62,7 @@ const primaryMenuItems = accountId => [
label: 'SETTINGS',
toState: frontendURL(`accounts/${accountId}/settings`),
toStateName: 'settings_home',
meta: {
permissions: [
'account_manage',
'conversation_manage',
'conversation_participating_manage',
'conversation_unassigned_manage',
],
},
roles: ['administrator', 'agent'],
},
];
@@ -20,9 +20,6 @@ const reports = accountId => ({
label: 'REPORTS_OVERVIEW',
hasSubMenu: false,
toState: frontendURL(`accounts/${accountId}/reports/overview`),
meta: {
permissions: ['account_manage'],
},
toStateName: 'account_overview_reports',
},
{
@@ -30,9 +27,6 @@ const reports = accountId => ({
label: 'REPORTS_CONVERSATION',
hasSubMenu: false,
toState: frontendURL(`accounts/${accountId}/reports/conversation`),
meta: {
permissions: ['account_manage'],
},
toStateName: 'conversation_reports',
},
{
@@ -40,9 +34,6 @@ const reports = accountId => ({
label: 'CSAT',
hasSubMenu: false,
toState: frontendURL(`accounts/${accountId}/reports/csat`),
meta: {
permissions: ['account_manage'],
},
toStateName: 'csat_reports',
},
{
@@ -51,9 +42,6 @@ const reports = accountId => ({
hasSubMenu: false,
featureFlag: FEATURE_FLAGS.RESPONSE_BOT,
toState: frontendURL(`accounts/${accountId}/reports/bot`),
meta: {
permissions: ['account_manage'],
},
toStateName: 'bot_reports',
},
{
@@ -61,9 +49,6 @@ const reports = accountId => ({
label: 'REPORTS_AGENT',
hasSubMenu: false,
toState: frontendURL(`accounts/${accountId}/reports/agent`),
meta: {
permissions: ['account_manage'],
},
toStateName: 'agent_reports',
},
{
@@ -71,9 +56,6 @@ const reports = accountId => ({
label: 'REPORTS_LABEL',
hasSubMenu: false,
toState: frontendURL(`accounts/${accountId}/reports/label`),
meta: {
permissions: ['account_manage'],
},
toStateName: 'label_reports',
},
{
@@ -81,9 +63,6 @@ const reports = accountId => ({
label: 'REPORTS_INBOX',
hasSubMenu: false,
toState: frontendURL(`accounts/${accountId}/reports/inboxes`),
meta: {
permissions: ['account_manage'],
},
toStateName: 'inbox_reports',
},
{
@@ -91,9 +70,6 @@ const reports = accountId => ({
label: 'REPORTS_TEAM',
hasSubMenu: false,
toState: frontendURL(`accounts/${accountId}/reports/teams`),
meta: {
permissions: ['account_manage'],
},
toStateName: 'team_reports',
},
{
@@ -102,9 +78,6 @@ const reports = accountId => ({
hasSubMenu: false,
featureFlag: FEATURE_FLAGS.SLA,
toState: frontendURL(`accounts/${accountId}/reports/sla`),
meta: {
permissions: ['account_manage'],
},
toStateName: 'sla_reports',
},
],
@@ -47,9 +47,6 @@ const settings = accountId => ({
label: 'ACCOUNT_SETTINGS',
hasSubMenu: false,
toState: frontendURL(`accounts/${accountId}/settings/general`),
meta: {
permissions: ['account_manage'],
},
toStateName: 'general_settings_index',
},
{
@@ -58,9 +55,6 @@ const settings = accountId => ({
hasSubMenu: false,
toState: frontendURL(`accounts/${accountId}/settings/agents/list`),
toStateName: 'agent_list',
meta: {
permissions: ['account_manage'],
},
featureFlag: FEATURE_FLAGS.AGENT_MANAGEMENT,
},
{
@@ -69,9 +63,6 @@ const settings = accountId => ({
hasSubMenu: false,
toState: frontendURL(`accounts/${accountId}/settings/teams/list`),
toStateName: 'settings_teams_list',
meta: {
permissions: ['account_manage'],
},
featureFlag: FEATURE_FLAGS.TEAM_MANAGEMENT,
},
{
@@ -80,9 +71,6 @@ const settings = accountId => ({
hasSubMenu: false,
toState: frontendURL(`accounts/${accountId}/settings/inboxes/list`),
toStateName: 'settings_inbox_list',
meta: {
permissions: ['account_manage'],
},
featureFlag: FEATURE_FLAGS.INBOX_MANAGEMENT,
},
{
@@ -91,9 +79,6 @@ const settings = accountId => ({
hasSubMenu: false,
toState: frontendURL(`accounts/${accountId}/settings/labels/list`),
toStateName: 'labels_list',
meta: {
permissions: ['account_manage'],
},
featureFlag: FEATURE_FLAGS.LABELS,
},
{
@@ -104,9 +89,6 @@ const settings = accountId => ({
`accounts/${accountId}/settings/custom-attributes/list`
),
toStateName: 'attributes_list',
meta: {
permissions: ['account_manage'],
},
featureFlag: FEATURE_FLAGS.CUSTOM_ATTRIBUTES,
},
{
@@ -115,9 +97,6 @@ const settings = accountId => ({
hasSubMenu: false,
toState: frontendURL(`accounts/${accountId}/settings/automation/list`),
toStateName: 'automation_list',
meta: {
permissions: ['account_manage'],
},
featureFlag: FEATURE_FLAGS.AUTOMATIONS,
},
{
@@ -127,9 +106,6 @@ const settings = accountId => ({
globalConfigFlag: 'csmlEditorHost',
toState: frontendURL(`accounts/${accountId}/settings/agent-bots`),
toStateName: 'agent_bots',
meta: {
permissions: ['account_manage'],
},
featureFlag: FEATURE_FLAGS.AGENT_BOTS,
},
{
@@ -138,9 +114,6 @@ const settings = accountId => ({
hasSubMenu: false,
toState: frontendURL(`accounts/${accountId}/settings/macros`),
toStateName: 'macros_wrapper',
meta: {
permissions: ['account_manage'],
},
featureFlag: FEATURE_FLAGS.MACROS,
},
{
@@ -151,9 +124,6 @@ const settings = accountId => ({
`accounts/${accountId}/settings/canned-response/list`
),
toStateName: 'canned_list',
meta: {
permissions: ['account_manage'],
},
featureFlag: FEATURE_FLAGS.CANNED_RESPONSES,
},
{
@@ -162,9 +132,6 @@ const settings = accountId => ({
hasSubMenu: false,
toState: frontendURL(`accounts/${accountId}/settings/integrations`),
toStateName: 'settings_integrations',
meta: {
permissions: ['account_manage'],
},
featureFlag: FEATURE_FLAGS.INTEGRATIONS,
},
{
@@ -173,9 +140,6 @@ const settings = accountId => ({
hasSubMenu: false,
toState: frontendURL(`accounts/${accountId}/settings/applications`),
toStateName: 'settings_applications',
meta: {
permissions: ['account_manage'],
},
featureFlag: FEATURE_FLAGS.INTEGRATIONS,
},
{
@@ -186,9 +150,6 @@ const settings = accountId => ({
toStateName: 'auditlogs_list',
isEnterpriseOnly: true,
featureFlag: FEATURE_FLAGS.AUDIT_LOGS,
meta: {
permissions: ['account_manage'],
},
beta: true,
},
{
@@ -199,9 +160,6 @@ const settings = accountId => ({
toStateName: 'sla_list',
isEnterpriseOnly: true,
featureFlag: FEATURE_FLAGS.SLA,
meta: {
permissions: ['account_manage'],
},
beta: true,
},
{
@@ -210,9 +168,6 @@ const settings = accountId => ({
hasSubMenu: false,
toState: frontendURL(`accounts/${accountId}/settings/billing`),
toStateName: 'billing_settings_index',
meta: {
permissions: ['account_manage'],
},
showOnlyOnCloud: true,
},
],
@@ -60,9 +60,9 @@ export default {
type: Object,
default: () => {},
},
currentUser: {
type: Object,
default: () => {},
currentRole: {
type: String,
default: '',
},
isOnChatwootCloud: {
type: Boolean,
@@ -80,16 +80,16 @@ export default {
return this.customViews.filter(view => view.filter_type === 'contact');
},
accessibleMenuItems() {
const menuItemsFilteredByPermissions = this.menuConfig.menuItems.filter(
menuItem => {
const { meta: { permissions: menuPermissions = [] } = {} } = menuItem;
const { permissions: userPermissions = [] } = this.currentUser;
return menuPermissions.some(permission =>
userPermissions.includes(permission)
);
}
if (!this.currentRole) {
return [];
}
const menuItemsFilteredByRole = this.menuConfig.menuItems.filter(
menuItem =>
window.roleWiseRoutes[this.currentRole].indexOf(
menuItem.toStateName
) > -1
);
return menuItemsFilteredByPermissions.filter(item => {
return menuItemsFilteredByRole.filter(item => {
if (item.showOnlyOnCloud) {
return this.isOnChatwootCloud;
}
@@ -65,29 +65,27 @@
:show-child-count="showChildCount(child.count)"
:child-item-count="child.count"
/>
<Policy :permissions="['account_manage']">
<router-link
v-if="menuItem.newLink"
v-slot="{ href, navigate }"
:to="menuItem.toState"
custom
>
<li class="pl-1">
<a :href="href">
<woot-button
size="tiny"
variant="clear"
color-scheme="secondary"
icon="add"
:data-testid="menuItem.dataTestid"
@click="e => newLinkClick(e, navigate)"
>
{{ $t(`SIDEBAR.${menuItem.newLinkTag}`) }}
</woot-button>
</a>
</li>
</router-link>
</Policy>
<router-link
v-if="showItem(menuItem)"
v-slot="{ href, navigate }"
:to="menuItem.toState"
custom
>
<li class="pl-1">
<a :href="href">
<woot-button
size="tiny"
variant="clear"
color-scheme="secondary"
icon="add"
:data-testid="menuItem.dataTestid"
@click="e => newLinkClick(e, navigate)"
>
{{ $t(`SIDEBAR.${menuItem.newLinkTag}`) }}
</woot-button>
</a>
</li>
</router-link>
</ul>
</li>
</template>
@@ -107,10 +105,9 @@ import {
isOnMentionsView,
isOnUnattendedView,
} from '../../../store/modules/conversations/helpers/actionHelpers';
import Policy from '../../policy.vue';
export default {
components: { SecondaryChildNavItem, Policy },
components: { SecondaryChildNavItem },
mixins: [adminMixin, configMixin],
props: {
menuItem: {
@@ -1,24 +0,0 @@
<script setup>
import { useStoreGetters } from 'dashboard/composables/store';
import { computed } from 'vue';
const props = defineProps({
permissions: {
type: Array,
required: true,
},
});
const getters = useStoreGetters();
const user = getters.getCurrentUser.value;
const hasPermission = computed(() =>
props.permissions.some(permission => user.permissions.includes(permission))
);
</script>
<template>
<div v-if="hasPermission">
<slot />
</div>
</template>
@@ -13,7 +13,7 @@ defineProps({
<template>
<button
class="relative inline-flex items-center justify-start w-full p-3 border-0 rounded-none first:rounded-t-xl last:rounded-b-xl h-11 hover:bg-slate-50 dark:hover:bg-slate-700 active:bg-slate-75 dark:active:bg-slate-800"
@click.stop.prevent="$emit('click')"
@click.stop="$emit('click')"
@mouseenter="$emit('mouseenter')"
@mouseleave="$emit('mouseleave')"
@focus="$emit('focus')"
@@ -1,9 +1,5 @@
<template>
<li
v-if="shouldRenderMessage"
:id="`message${data.id}`"
:class="[alignBubble, 'group']"
>
<li v-if="shouldRenderMessage" :id="`message${data.id}`" :class="alignBubble">
<div :class="wrapClass">
<div
v-if="isFailed && !hasOneDayPassed && !isAnEmailInbox"
@@ -125,10 +121,7 @@
</a>
</div>
</div>
<div
v-if="shouldShowContextMenu"
class="context-menu-wrap invisible group-hover:visible"
>
<div v-if="shouldShowContextMenu" class="context-menu-wrap">
<context-menu
v-if="isBubble && !isMessageDeleted"
:context-menu-position="contextMenuPosition"
@@ -257,16 +250,7 @@ export default {
html_content: { full: fullHTMLContent } = {},
text_content: { full: fullTextContent } = {},
} = this.contentAttributes.email || {};
if (fullHTMLContent) {
return fullHTMLContent;
}
if (fullTextContent) {
return fullTextContent.replace(/\n/g, '<br>');
}
return '';
return fullHTMLContent || fullTextContent || '';
},
displayQuotedButton() {
if (this.emailMessageContent.includes('<blockquote')) {
@@ -1,5 +1,5 @@
<template>
<div>
<div @submit.prevent="onSubmit">
<woot-input
v-model="formState.title"
:class="{ error: v$.title.$error }"
@@ -26,19 +26,61 @@
"
/>
</label>
<div class="flex flex-col gap-4">
<searchable-dropdown
v-for="dropdown in dropdowns"
:key="dropdown.type"
:type="dropdown.type"
:value="formState[dropdown.type]"
:label="$t(dropdown.label)"
:items="dropdown.items"
:placeholder="$t(dropdown.placeholder)"
:error="dropdown.error"
@change="onChange"
/>
</div>
<label :class="{ error: v$.teamId.$error }">
{{ $t('INTEGRATION_SETTINGS.LINEAR.ADD_OR_LINK.FORM.TEAM.LABEL') }}
<select
v-model="formState.teamId"
:style="inputStyles"
@change="onChangeTeam"
>
<option v-for="item in teams" :key="item.name" :value="item.id">
{{ item.name }}
</option>
</select>
<span v-if="v$.teamId.$error" class="message">
{{ teamError }}
</span>
</label>
<label>
{{ $t('INTEGRATION_SETTINGS.LINEAR.ADD_OR_LINK.FORM.ASSIGNEE.LABEL') }}
<select v-model="formState.assigneeId" :style="inputStyles">
<option v-for="item in assignees" :key="item.name" :value="item.id">
{{ item.name }}
</option>
</select>
</label>
<label>
{{ $t('INTEGRATION_SETTINGS.LINEAR.ADD_OR_LINK.FORM.LABEL.LABEL') }}
<select v-model="formState.labelId" :style="inputStyles">
<option v-for="item in labels" :key="item.name" :value="item.id">
{{ item.name }}
</option>
</select>
</label>
<label>
{{ $t('INTEGRATION_SETTINGS.LINEAR.ADD_OR_LINK.FORM.PRIORITY.LABEL') }}
<select v-model="formState.priority" :style="inputStyles">
<option v-for="item in priorities" :key="item.name" :value="item.id">
{{ item.name }}
</option>
</select>
</label>
<label>
{{ $t('INTEGRATION_SETTINGS.LINEAR.ADD_OR_LINK.FORM.PROJECT.LABEL') }}
<select v-model="formState.projectId" :style="inputStyles">
<option v-for="item in projects" :key="item.name" :value="item.id">
{{ item.name }}
</option>
</select>
</label>
<label>
{{ $t('INTEGRATION_SETTINGS.LINEAR.ADD_OR_LINK.FORM.STATUS.LABEL') }}
<select v-model="formState.stateId" :style="inputStyles">
<option v-for="item in statuses" :key="item.name" :value="item.id">
{{ item.name }}
</option>
</select>
</label>
<div class="flex items-center justify-end w-full gap-2 mt-8">
<woot-button
class="px-4 rounded-xl button clear outline-woot-200/50 outline"
@@ -66,7 +108,6 @@ import { useAlert } from 'dashboard/composables';
import LinearAPI from 'dashboard/api/integrations/linear';
import validations from './validations';
import { parseLinearAPIErrorResponse } from 'dashboard/store/utils/api';
import SearchableDropdown from './SearchableDropdown.vue';
const props = defineProps({
accountId: {
@@ -121,6 +162,7 @@ const formState = reactive({
priority: '',
projectId: '',
});
const v$ = useVuelidate(validations, formState);
const isSubmitDisabled = computed(
@@ -137,56 +179,6 @@ const teamError = computed(() =>
: ''
);
const dropdowns = computed(() => {
return [
{
type: 'teamId',
label: 'INTEGRATION_SETTINGS.LINEAR.ADD_OR_LINK.FORM.TEAM.LABEL',
items: teams.value,
placeholder: 'INTEGRATION_SETTINGS.LINEAR.ADD_OR_LINK.FORM.TEAM.SEARCH',
error: teamError.value,
},
{
type: 'assigneeId',
label: 'INTEGRATION_SETTINGS.LINEAR.ADD_OR_LINK.FORM.ASSIGNEE.LABEL',
items: assignees.value,
placeholder:
'INTEGRATION_SETTINGS.LINEAR.ADD_OR_LINK.FORM.ASSIGNEE.SEARCH',
error: '',
},
{
type: 'labelId',
label: 'INTEGRATION_SETTINGS.LINEAR.ADD_OR_LINK.FORM.LABEL.LABEL',
items: labels.value,
placeholder: 'INTEGRATION_SETTINGS.LINEAR.ADD_OR_LINK.FORM.LABEL.SEARCH',
error: '',
},
{
type: 'priority',
label: 'INTEGRATION_SETTINGS.LINEAR.ADD_OR_LINK.FORM.PRIORITY.LABEL',
items: priorities,
placeholder:
'INTEGRATION_SETTINGS.LINEAR.ADD_OR_LINK.FORM.PRIORITY.SEARCH',
error: '',
},
{
type: 'projectId',
label: 'INTEGRATION_SETTINGS.LINEAR.ADD_OR_LINK.FORM.PROJECT.LABEL',
items: projects.value,
placeholder:
'INTEGRATION_SETTINGS.LINEAR.ADD_OR_LINK.FORM.PROJECT.SEARCH',
error: '',
},
{
type: 'stateId',
label: 'INTEGRATION_SETTINGS.LINEAR.ADD_OR_LINK.FORM.STATUS.LABEL',
items: statuses.value,
placeholder: 'INTEGRATION_SETTINGS.LINEAR.ADD_OR_LINK.FORM.STATUS.SEARCH',
error: '',
},
];
});
const onClose = () => emit('close');
const getTeams = async () => {
@@ -220,15 +212,12 @@ const getTeamEntities = async () => {
}
};
const onChange = (item, type) => {
formState[type] = item.id;
if (type === 'teamId') {
formState.assigneeId = '';
formState.stateId = '';
formState.labelId = '';
formState.projectId = '';
getTeamEntities();
}
const onChangeTeam = event => {
formState.teamId = event.target.value;
formState.assigneeId = '';
formState.stateId = '';
formState.labelId = '';
getTeamEntities();
};
const createIssue = async () => {
@@ -6,7 +6,7 @@
<filter-button
right-icon="chevron-down"
:button-text="linkIssueTitle"
class="justify-between w-full h-[2.5rem] py-1.5 px-3 rounded-xl border border-slate-50 bg-slate-25 dark:border-slate-600 dark:bg-slate-900 hover:bg-slate-50 dark:hover:bg-slate-900/50"
class="justify-between w-full bg-slate-50 dark:bg-slate-800 hover:bg-slate-75 dark:hover:bg-slate-800"
@click="toggleDropdown"
>
<template v-if="shouldShowDropdown" #dropdown>
@@ -26,7 +26,7 @@
/>
</template>
</filter-button>
<div class="flex items-center justify-end w-full gap-2 mt-2">
<div class="flex items-center justify-end w-full gap-2">
<woot-button
class="px-4 rounded-xl button clear outline-woot-200/50 outline"
@click.prevent="onClose"
@@ -1,73 +0,0 @@
<template>
<div
class="flex w-full"
:class="type === 'stateId' && shouldShowDropdown ? 'h-[150px]' : 'gap-2'"
>
<label class="w-full" :class="{ error: hasError }">
{{ label }}
<filter-button
right-icon="chevron-down"
:button-text="selectedItemName"
class="justify-between w-full h-[2.5rem] py-1.5 px-3 rounded-xl border border-slate-50 bg-slate-25 dark:border-slate-600 dark:bg-slate-900 hover:bg-slate-50 dark:hover:bg-slate-900/50"
@click="toggleDropdown"
>
<template v-if="shouldShowDropdown" #dropdown>
<filter-list-dropdown
v-on-clickaway="toggleDropdown"
:show-clear-filter="false"
:list-items="items"
:active-filter-id="selectedItemId"
:input-placeholder="placeholder"
enable-search
class="left-0 flex flex-col w-full overflow-y-auto h-fit !max-h-[160px] md:left-auto md:right-0 top-10"
@click="onSelect"
/>
</template>
</filter-button>
<span v-if="hasError" class="mt-1 message">{{ error }}</span>
</label>
</div>
</template>
<script setup>
import { ref, computed, defineComponent } from 'vue';
import FilterButton from 'dashboard/components/ui/Dropdown/DropdownButton.vue';
import FilterListDropdown from 'dashboard/components/ui/Dropdown/DropdownList.vue';
defineComponent({
name: 'SearchableDropdown',
});
const props = defineProps({
type: { type: String, required: true },
label: { type: String, default: null },
items: { type: Array, required: true },
value: { type: [Number, String], default: null },
placeholder: { type: String, default: null },
error: { type: String, default: null },
});
const emit = defineEmits(['change']);
const shouldShowDropdown = ref(false);
const toggleDropdown = () => {
shouldShowDropdown.value = !shouldShowDropdown.value;
};
const onSelect = item => {
emit('change', item, props.type);
toggleDropdown();
};
const hasError = computed(() => !!props.error);
const selectedItem = computed(() => {
if (!props.value) return null;
return props.items.find(i => i.id === props.value);
});
const selectedItemName = computed(
() => selectedItem.value?.name || props.placeholder
);
const selectedItemId = computed(() => selectedItem.value?.id || null);
</script>
@@ -1,10 +0,0 @@
export const ACCOUNT_PERMISSIONS = {
ACCOUNT_MANAGE: 'account_manage',
PORTAL_MANAGE: 'portal_manage',
CONVERSATION_MANAGE: 'conversation_manage',
CONVERSATION_UNASSIGNED_MANAGE: 'conversation_unassigned_manage',
CONVERSATION_PARTICIPATING_MANAGE: 'conversation_participating_manage',
MACROS_MANAGE: 'macros_manage',
REPORTS_MANAGE: 'reports_manage',
CONTACTS_MANAGE: 'contacts_manage',
};
@@ -121,7 +121,7 @@ export const generateConditionOptions = (options, key = 'id') => {
};
// Add the "None" option to the agent list
export const addNoneToList = agents => [
export const agentList = agents => [
{
id: 'nil',
name: 'None',
@@ -137,8 +137,8 @@ export const getActionOptions = ({
type,
}) => {
const actionsMap = {
assign_agent: addNoneToList(agents),
assign_team: addNoneToList(teams),
assign_agent: agentList(agents),
assign_team: teams,
send_email_to_team: teams,
add_label: generateConditionOptions(labels, 'title'),
remove_label: generateConditionOptions(labels, 'title'),
@@ -3,14 +3,17 @@ export const getCurrentAccount = ({ accounts } = {}, accountId) => {
return accounts.find(account => account.id === accountId);
};
export const routeIsAccessibleFor = (route, userPermissions = []) => {
const { meta: { permissions: routePermissions = [] } = {} } = route;
return routePermissions.some(permissions =>
userPermissions.includes(permissions)
);
// eslint-disable-next-line default-param-last
export const getUserRole = ({ accounts } = {}, accountId) => {
const currentAccount = getCurrentAccount({ accounts }, accountId) || {};
return currentAccount.role || null;
};
const validateActiveAccountRoutes = (to, user) => {
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`;
@@ -19,13 +22,15 @@ const validateActiveAccountRoutes = (to, user) => {
return accountDashboardURL;
}
const isAccessible = routeIsAccessibleFor(to, user.permissions);
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) => {
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) {
@@ -35,7 +40,7 @@ export const validateLoggedInRoutes = (to, user) => {
const isCurrentAccountActive = currentAccount.status === 'active';
if (isCurrentAccountActive) {
return validateActiveAccountRoutes(to, user);
return validateActiveAccountRoutes(to, user, roleWiseRoutes);
}
// If the current account is not active, then redirect the user to the suspended screen
@@ -367,14 +367,8 @@
"DESCRIPTION": "Click on the Sign in with Microsoft button to get started. You will redirected to the email sign in page. Once you accept the requested permissions, you would be redirected back to the inbox creation step.",
"EMAIL_PLACEHOLDER": "Enter email address",
"SIGN_IN": "Sign in with Microsoft",
"HELP": "To add your Microsoft account as a channel, you need to authenticate your Microsoft account by clicking on 'Sign in with Microsoft' ",
"ERROR_MESSAGE": "There was an error connecting to Microsoft, please try again"
},
"GOOGLE": {
"TITLE": "Google Email",
"DESCRIPTION": "Click on the Sign in with Google button to get started. You will redirected to the email sign in page. Once you accept the requested permissions, you would be redirected back to the inbox creation step.",
"SIGN_IN": "Sign in with Google",
"EMAIL_PLACEHOLDER": "Enter email address",
"ERROR_MESSAGE": "There was an error connecting to Google, please try again"
}
},
"DETAILS": {
@@ -742,7 +736,6 @@
},
"EMAIL_PROVIDERS": {
"MICROSOFT": "Microsoft",
"GOOGLE": "Google",
"OTHER_PROVIDERS": "Other Providers"
}
}
@@ -7,14 +7,7 @@ export const routes = [
{
path: frontendURL('accounts/:accountId/search'),
name: 'search',
meta: {
permissions: [
'account_manage',
'conversation_manage',
'conversation_participating_manage',
'conversation_unassigned_manage',
],
},
roles: ['administrator', 'agent'],
component: SearchView,
},
];
@@ -7,27 +7,13 @@ export const routes = [
{
path: frontendURL('accounts/:accountId/contacts'),
name: 'contacts_dashboard',
meta: {
permissions: [
'account_manage',
'conversation_manage',
'conversation_participating_manage',
'conversation_unassigned_manage',
],
},
roles: ['administrator', 'agent'],
component: ContactsView,
},
{
path: frontendURL('accounts/:accountId/contacts/custom_view/:id'),
name: 'contacts_segments_dashboard',
meta: {
permissions: [
'account_manage',
'conversation_manage',
'conversation_participating_manage',
'conversation_unassigned_manage',
],
},
roles: ['administrator', 'agent'],
component: ContactsView,
props: route => {
return { segmentsId: route.params.id };
@@ -36,14 +22,7 @@ export const routes = [
{
path: frontendURL('accounts/:accountId/labels/:label/contacts'),
name: 'contacts_labels_dashboard',
meta: {
permissions: [
'account_manage',
'conversation_manage',
'conversation_participating_manage',
'conversation_unassigned_manage',
],
},
roles: ['administrator', 'agent'],
component: ContactsView,
props: route => {
return { label: route.params.label };
@@ -52,14 +31,7 @@ export const routes = [
{
path: frontendURL('accounts/:accountId/contacts/:contactId'),
name: 'contact_profile_dashboard',
meta: {
permissions: [
'account_manage',
'conversation_manage',
'conversation_participating_manage',
'conversation_unassigned_manage',
],
},
roles: ['administrator', 'agent'],
component: ContactManageView,
props: route => {
return { contactId: route.params.contactId };
@@ -1,21 +1,13 @@
/* eslint arrow-body-style: 0 */
import { ACCOUNT_PERMISSIONS } from '../../../constants/permissions';
import { frontendURL } from '../../../helper/URLHelper';
const ConversationView = () => import('./ConversationView');
const GENERIC_CONVERSATION_PERMISSIONS = [
ACCOUNT_PERMISSIONS.ACCOUNT_MANAGE,
ACCOUNT_PERMISSIONS.CONVERSATION_MANAGE,
ACCOUNT_PERMISSIONS.CONVERSATION_PARTICIPATING_MANAGE,
ACCOUNT_PERMISSIONS.CONVERSATION_UNASSIGNED_MANAGE,
];
export default {
routes: [
{
path: frontendURL('accounts/:accountId/dashboard'),
name: 'home',
meta: { permissions: GENERIC_CONVERSATION_PERMISSIONS },
roles: ['administrator', 'agent'],
component: ConversationView,
props: () => {
return { inboxId: 0 };
@@ -24,7 +16,7 @@ export default {
{
path: frontendURL('accounts/:accountId/conversations/:conversation_id'),
name: 'inbox_conversation',
meta: { permissions: GENERIC_CONVERSATION_PERMISSIONS },
roles: ['administrator', 'agent'],
component: ConversationView,
props: route => {
return { inboxId: 0, conversationId: route.params.conversation_id };
@@ -33,7 +25,7 @@ export default {
{
path: frontendURL('accounts/:accountId/inbox/:inbox_id'),
name: 'inbox_dashboard',
meta: { permissions: GENERIC_CONVERSATION_PERMISSIONS },
roles: ['administrator', 'agent'],
component: ConversationView,
props: route => {
return { inboxId: route.params.inbox_id };
@@ -44,7 +36,7 @@ export default {
'accounts/:accountId/inbox/:inbox_id/conversations/:conversation_id'
),
name: 'conversation_through_inbox',
meta: { permissions: GENERIC_CONVERSATION_PERMISSIONS },
roles: ['administrator', 'agent'],
component: ConversationView,
props: route => {
return {
@@ -56,7 +48,7 @@ export default {
{
path: frontendURL('accounts/:accountId/label/:label'),
name: 'label_conversations',
meta: { permissions: GENERIC_CONVERSATION_PERMISSIONS },
roles: ['administrator', 'agent'],
component: ConversationView,
props: route => ({ label: route.params.label }),
},
@@ -65,7 +57,7 @@ export default {
'accounts/:accountId/label/:label/conversations/:conversation_id'
),
name: 'conversations_through_label',
meta: { permissions: GENERIC_CONVERSATION_PERMISSIONS },
roles: ['administrator', 'agent'],
component: ConversationView,
props: route => ({
conversationId: route.params.conversation_id,
@@ -75,7 +67,7 @@ export default {
{
path: frontendURL('accounts/:accountId/team/:teamId'),
name: 'team_conversations',
meta: { permissions: GENERIC_CONVERSATION_PERMISSIONS },
roles: ['administrator', 'agent'],
component: ConversationView,
props: route => ({ teamId: route.params.teamId }),
},
@@ -84,7 +76,7 @@ export default {
'accounts/:accountId/team/:teamId/conversations/:conversationId'
),
name: 'conversations_through_team',
meta: { permissions: GENERIC_CONVERSATION_PERMISSIONS },
roles: ['administrator', 'agent'],
component: ConversationView,
props: route => ({
conversationId: route.params.conversationId,
@@ -94,7 +86,7 @@ export default {
{
path: frontendURL('accounts/:accountId/custom_view/:id'),
name: 'folder_conversations',
meta: { permissions: GENERIC_CONVERSATION_PERMISSIONS },
roles: ['administrator', 'agent'],
component: ConversationView,
props: route => ({ foldersId: route.params.id }),
},
@@ -103,7 +95,7 @@ export default {
'accounts/:accountId/custom_view/:id/conversations/:conversation_id'
),
name: 'conversations_through_folders',
meta: { permissions: GENERIC_CONVERSATION_PERMISSIONS },
roles: ['administrator', 'agent'],
component: ConversationView,
props: route => ({
conversationId: route.params.conversation_id,
@@ -113,7 +105,7 @@ export default {
{
path: frontendURL('accounts/:accountId/mentions/conversations'),
name: 'conversation_mentions',
meta: { permissions: GENERIC_CONVERSATION_PERMISSIONS },
roles: ['administrator', 'agent'],
component: ConversationView,
props: () => ({ conversationType: 'mention' }),
},
@@ -122,7 +114,7 @@ export default {
'accounts/:accountId/mentions/conversations/:conversationId'
),
name: 'conversation_through_mentions',
meta: { permissions: GENERIC_CONVERSATION_PERMISSIONS },
roles: ['administrator', 'agent'],
component: ConversationView,
props: route => ({
conversationId: route.params.conversationId,
@@ -132,7 +124,7 @@ export default {
{
path: frontendURL('accounts/:accountId/unattended/conversations'),
name: 'conversation_unattended',
meta: { permissions: GENERIC_CONVERSATION_PERMISSIONS },
roles: ['administrator', 'agent'],
component: ConversationView,
props: () => ({ conversationType: 'unattended' }),
},
@@ -141,7 +133,7 @@ export default {
'accounts/:accountId/unattended/conversations/:conversationId'
),
name: 'conversation_through_unattended',
meta: { permissions: GENERIC_CONVERSATION_PERMISSIONS },
roles: ['administrator', 'agent'],
component: ConversationView,
props: route => ({
conversationId: route.params.conversationId,
@@ -151,13 +143,8 @@ export default {
{
path: frontendURL('accounts/:accountId/participating/conversations'),
name: 'conversation_participating',
roles: ['administrator', 'agent'],
component: ConversationView,
meta: {
permissions: [
ACCOUNT_PERMISSIONS.CONVERSATION_MANAGE,
ACCOUNT_PERMISSIONS.CONVERSATION_PARTICIPATING_MANAGE,
],
},
props: () => ({ conversationType: 'participating' }),
},
{
@@ -165,13 +152,8 @@ export default {
'accounts/:accountId/participating/conversations/:conversationId'
),
name: 'conversation_through_participating',
roles: ['administrator', 'agent'],
component: ConversationView,
meta: {
permissions: [
ACCOUNT_PERMISSIONS.CONVERSATION_MANAGE,
ACCOUNT_PERMISSIONS.CONVERSATION_PARTICIPATING_MANAGE,
],
},
props: route => ({
conversationId: route.params.conversationId,
conversationType: 'participating',
@@ -28,9 +28,7 @@ export default {
{
path: frontendURL('accounts/:accountId/suspended'),
name: 'account_suspended',
meta: {
permissions: ['profile_manage'],
},
roles: ['administrator', 'agent'],
component: Suspended,
},
],
@@ -30,27 +30,13 @@ const portalRoutes = [
{
path: getPortalRoute(''),
name: 'default_portal_articles',
meta: {
permissions: [
'account_manage',
'conversation_manage',
'conversation_participating_manage',
'conversation_unassigned_manage',
],
},
roles: ['administrator', 'agent'],
component: DefaultPortalArticles,
},
{
path: getPortalRoute('all'),
name: 'list_all_portals',
meta: {
permissions: [
'account_manage',
'conversation_manage',
'conversation_participating_manage',
'conversation_unassigned_manage',
],
},
roles: ['administrator', 'agent'],
component: ListAllPortals,
},
{
@@ -61,88 +47,55 @@ const portalRoutes = [
path: '',
name: 'new_portal_information',
component: PortalDetails,
meta: {
permissions: ['account_manage'],
},
roles: ['administrator'],
},
{
path: ':portalSlug/customization',
name: 'portal_customization',
component: PortalCustomization,
meta: {
permissions: ['account_manage'],
},
roles: ['administrator'],
},
{
path: ':portalSlug/finish',
name: 'portal_finish',
component: PortalSettingsFinish,
meta: {
permissions: ['account_manage'],
},
roles: ['administrator'],
},
],
},
{
path: getPortalRoute(':portalSlug'),
name: 'portalSlug',
meta: {
permissions: [
'account_manage',
'conversation_manage',
'conversation_participating_manage',
'conversation_unassigned_manage',
],
},
roles: ['administrator', 'agent'],
component: ShowPortal,
},
{
path: getPortalRoute(':portalSlug/edit'),
meta: {
permissions: [
'account_manage',
'conversation_manage',
'conversation_participating_manage',
'conversation_unassigned_manage',
],
},
roles: ['administrator', 'agent'],
component: EditPortal,
children: [
{
path: '',
name: 'edit_portal_information',
component: EditPortalBasic,
meta: {
permissions: ['account_manage'],
},
roles: ['administrator'],
},
{
path: 'customizations',
name: 'edit_portal_customization',
component: EditPortalCustomization,
meta: {
permissions: ['account_manage'],
},
roles: ['administrator'],
},
{
path: 'locales',
name: 'edit_portal_locales',
component: EditPortalLocales,
meta: {
permissions: ['account_manage'],
},
roles: ['administrator'],
},
{
path: 'categories',
name: 'list_all_locale_categories',
meta: {
permissions: [
'account_manage',
'conversation_manage',
'conversation_participating_manage',
'conversation_unassigned_manage',
],
},
roles: ['administrator', 'agent'],
component: ListAllCategories,
},
],
@@ -153,81 +106,39 @@ const articleRoutes = [
{
path: getPortalRoute(':portalSlug/:locale/articles'),
name: 'list_all_locale_articles',
meta: {
permissions: [
'account_manage',
'conversation_manage',
'conversation_participating_manage',
'conversation_unassigned_manage',
],
},
roles: ['administrator', 'agent'],
component: ListAllArticles,
},
{
path: getPortalRoute(':portalSlug/:locale/articles/new'),
name: 'new_article',
meta: {
permissions: [
'account_manage',
'conversation_manage',
'conversation_participating_manage',
'conversation_unassigned_manage',
],
},
roles: ['administrator', 'agent'],
component: NewArticle,
},
{
path: getPortalRoute(':portalSlug/:locale/articles/mine'),
name: 'list_mine_articles',
meta: {
permissions: [
'account_manage',
'conversation_manage',
'conversation_participating_manage',
'conversation_unassigned_manage',
],
},
roles: ['administrator', 'agent'],
component: ListAllArticles,
},
{
path: getPortalRoute(':portalSlug/:locale/articles/archived'),
name: 'list_archived_articles',
meta: {
permissions: [
'account_manage',
'conversation_manage',
'conversation_participating_manage',
'conversation_unassigned_manage',
],
},
roles: ['administrator', 'agent'],
component: ListAllArticles,
},
{
path: getPortalRoute(':portalSlug/:locale/articles/draft'),
name: 'list_draft_articles',
meta: {
permissions: [
'account_manage',
'conversation_manage',
'conversation_participating_manage',
'conversation_unassigned_manage',
],
},
roles: ['administrator', 'agent'],
component: ListAllArticles,
},
{
path: getPortalRoute(':portalSlug/:locale/articles/:articleSlug'),
name: 'edit_article',
meta: {
permissions: [
'account_manage',
'conversation_manage',
'conversation_participating_manage',
'conversation_unassigned_manage',
],
},
roles: ['administrator', 'agent'],
component: EditArticle,
},
];
@@ -236,40 +147,19 @@ const categoryRoutes = [
{
path: getPortalRoute(':portalSlug/:locale/categories'),
name: 'all_locale_categories',
meta: {
permissions: [
'account_manage',
'conversation_manage',
'conversation_participating_manage',
'conversation_unassigned_manage',
],
},
roles: ['administrator', 'agent'],
component: ListAllCategories,
},
{
path: getPortalRoute(':portalSlug/:locale/categories/new'),
name: 'new_category_in_locale',
meta: {
permissions: [
'account_manage',
'conversation_manage',
'conversation_participating_manage',
'conversation_unassigned_manage',
],
},
roles: ['administrator', 'agent'],
component: NewCategory,
},
{
path: getPortalRoute(':portalSlug/:locale/categories/:categorySlug'),
name: 'show_category',
meta: {
permissions: [
'account_manage',
'conversation_manage',
'conversation_participating_manage',
'conversation_unassigned_manage',
],
},
roles: ['administrator', 'agent'],
component: ListAllArticles,
},
{
@@ -277,27 +167,13 @@ const categoryRoutes = [
':portalSlug/:locale/categories/:categorySlug/articles'
),
name: 'show_category_articles',
meta: {
permissions: [
'account_manage',
'conversation_manage',
'conversation_participating_manage',
'conversation_unassigned_manage',
],
},
roles: ['administrator', 'agent'],
component: ListCategoryArticles,
},
{
path: getPortalRoute(':portalSlug/:locale/categories/:categorySlug'),
name: 'edit_category',
meta: {
permissions: [
'account_manage',
'conversation_manage',
'conversation_participating_manage',
'conversation_unassigned_manage',
],
},
roles: ['administrator', 'agent'],
component: EditCategory,
},
];
@@ -12,27 +12,13 @@ export const routes = [
path: '',
name: 'inbox_view',
component: InboxEmptyStateView,
meta: {
permissions: [
'account_manage',
'conversation_manage',
'conversation_participating_manage',
'conversation_unassigned_manage',
],
},
roles: ['administrator', 'agent'],
},
{
path: ':notification_id',
name: 'inbox_view_conversation',
component: InboxDetailView,
meta: {
permissions: [
'account_manage',
'conversation_manage',
'conversation_participating_manage',
'conversation_unassigned_manage',
],
},
roles: ['administrator', 'agent'],
},
],
},
@@ -18,14 +18,7 @@ export const routes = [
path: '',
name: 'notifications_index',
component: NotificationsView,
meta: {
permissions: [
'account_manage',
'conversation_manage',
'conversation_participating_manage',
'conversation_unassigned_manage',
],
},
roles: ['administrator', 'agent'],
},
],
},
@@ -6,9 +6,7 @@ export default {
routes: [
{
path: frontendURL('accounts/:accountId/settings/general'),
meta: {
permissions: ['account_manage'],
},
roles: ['administrator'],
component: SettingsContent,
props: {
headerTitle: 'GENERAL_SETTINGS.TITLE',
@@ -20,9 +18,7 @@ export default {
path: '',
name: 'general_settings_index',
component: Index,
meta: {
permissions: ['account_manage'],
},
roles: ['administrator'],
},
],
},
@@ -8,9 +8,7 @@ export default {
routes: [
{
path: frontendURL('accounts/:accountId/settings/agent-bots'),
meta: {
permissions: ['account_manage'],
},
roles: ['administrator'],
component: SettingsContent,
props: {
headerTitle: 'AGENT_BOTS.HEADER',
@@ -22,25 +20,19 @@ export default {
path: '',
name: 'agent_bots',
component: Bot,
meta: {
permissions: ['account_manage'],
},
roles: ['administrator'],
},
{
path: 'csml/new',
name: 'agent_bots_csml_new',
component: CsmlNewBot,
meta: {
permissions: ['account_manage'],
},
roles: ['administrator'],
},
{
path: 'csml/:botId',
name: 'agent_bots_csml_edit',
component: CsmlEditBot,
meta: {
permissions: ['account_manage'],
},
roles: ['administrator'],
},
],
},
@@ -22,9 +22,7 @@ export default {
path: 'list',
name: 'agent_list',
component: AgentHome,
meta: {
permissions: ['account_manage'],
},
roles: ['administrator'],
},
],
},
@@ -22,9 +22,7 @@ export default {
path: 'list',
name: 'attributes_list',
component: AttributesHome,
meta: {
permissions: ['account_manage'],
},
roles: ['administrator'],
},
],
},
@@ -22,9 +22,7 @@ export default {
{
path: 'list',
name: 'auditlogs_list',
meta: {
permissions: ['account_manage'],
},
roles: ['administrator'],
component: AuditLogsHome,
},
],
@@ -22,9 +22,7 @@ export default {
path: 'list',
name: 'automation_list',
component: Automation,
meta: {
permissions: ['account_manage'],
},
roles: ['administrator'],
},
],
},
@@ -6,9 +6,7 @@ export default {
routes: [
{
path: frontendURL('accounts/:accountId/settings/billing'),
meta: {
permissions: ['account_manage'],
},
roles: ['administrator'],
component: SettingsContent,
props: {
headerTitle: 'BILLING_SETTINGS.TITLE',
@@ -20,9 +18,7 @@ export default {
path: '',
name: 'billing_settings_index',
component: Index,
meta: {
permissions: ['account_manage'],
},
roles: ['administrator'],
},
],
},
@@ -19,9 +19,7 @@ export default {
{
path: 'ongoing',
name: 'ongoing_campaigns',
meta: {
permissions: ['account_manage'],
},
roles: ['administrator'],
component: Index,
},
],
@@ -37,9 +35,7 @@ export default {
{
path: 'one_off',
name: 'one_off',
meta: {
permissions: ['account_manage'],
},
roles: ['administrator'],
component: Index,
},
],
@@ -22,14 +22,7 @@ export default {
{
path: 'list',
name: 'canned_list',
meta: {
permissions: [
'account_manage',
'conversation_manage',
'conversation_participating_manage',
'conversation_unassigned_manage',
],
},
roles: ['administrator', 'agent'],
component: CannedHome,
},
],
@@ -541,8 +541,7 @@ export default {
this.isALineChannel ||
this.isAPIInbox ||
(this.isAnEmailChannel && !this.inbox.provider) ||
this.isAMicrosoftInbox ||
this.isAGoogleInbox ||
(this.isAnEmailChannel && this.inbox.provider === 'microsoft') ||
this.isAWhatsAppChannel ||
this.isAWebWidgetInbox
) {
@@ -8,7 +8,7 @@
:header-title="$t('INBOX_MGMT.ADD.EMAIL_PROVIDER.TITLE')"
:header-content="$t('INBOX_MGMT.ADD.EMAIL_PROVIDER.DESCRIPTION')"
/>
<div class="grid max-w-3xl grid-cols-4 mx-0 mt-6">
<div class="grid grid-cols-4 max-w-3xl mx-0 mt-6">
<channel-selector
v-for="emailProvider in emailProviderList"
:key="emailProvider.key"
@@ -20,59 +20,58 @@
</div>
</div>
<microsoft v-else-if="provider === 'microsoft'" />
<google v-else-if="provider === 'google'" />
<forward-to-option v-else-if="provider === 'other_provider'" />
</template>
<script setup>
import { ref, computed } from 'vue';
<script>
import ForwardToOption from './emailChannels/ForwardToOption.vue';
import Microsoft from './emailChannels/Microsoft.vue';
import Google from './emailChannels/Google.vue';
import { mapGetters } from 'vuex';
import ChannelSelector from 'dashboard/components/ChannelSelector.vue';
import PageHeader from '../../SettingsSubPageHeader.vue';
import { useStoreGetters } from 'dashboard/composables/store';
import { useI18n } from 'dashboard/composables/useI18n';
const provider = ref('');
const getters = useStoreGetters();
const { t } = useI18n();
const globalConfig = getters['globalConfig/get'];
const isAChatwootInstance = getters['globalConfig/isAChatwootInstance'];
const emailProviderList = computed(() => {
return [
{
title: t('INBOX_MGMT.EMAIL_PROVIDERS.MICROSOFT'),
isEnabled: !!globalConfig.value.azureAppId,
key: 'microsoft',
src: '/assets/images/dashboard/channels/microsoft.png',
export default {
components: {
ChannelSelector,
ForwardToOption,
Microsoft,
PageHeader,
},
data() {
return { provider: '' };
},
computed: {
...mapGetters({
globalConfig: 'globalConfig/get',
isAChatwootInstance: 'globalConfig/isAChatwootInstance',
}),
emailProviderList() {
return [
{
title: this.$t('INBOX_MGMT.EMAIL_PROVIDERS.MICROSOFT'),
isEnabled: !!this.globalConfig.azureAppId,
key: 'microsoft',
src: '/assets/images/dashboard/channels/microsoft.png',
},
{
title: this.$t('INBOX_MGMT.EMAIL_PROVIDERS.OTHER_PROVIDERS'),
isEnabled: true,
key: 'other_provider',
src: '/assets/images/dashboard/channels/email.png',
},
].filter(provider => {
if (this.isAChatwootInstance) {
return true;
}
return provider.isEnabled;
});
},
{
title: t('INBOX_MGMT.EMAIL_PROVIDERS.GOOGLE'),
isEnabled: !!window.chatwootConfig.googleOAuthClientId,
key: 'google',
src: '/assets/images/dashboard/channels/google.png',
},
methods: {
onClick(emailProvider) {
if (emailProvider.isEnabled) {
this.provider = emailProvider.key;
}
},
{
title: t('INBOX_MGMT.EMAIL_PROVIDERS.OTHER_PROVIDERS'),
isEnabled: true,
key: 'other_provider',
src: '/assets/images/dashboard/channels/email.png',
},
].filter(providerConfig => {
if (isAChatwootInstance.value) {
return true;
}
return providerConfig.isEnabled;
});
});
function onClick(emailProvider) {
if (emailProvider.isEnabled) {
this.provider = emailProvider.key;
}
}
},
};
</script>
@@ -1,18 +0,0 @@
<script setup>
import OAuthChannel from './OAuthChannel.vue';
import { defineComponent } from 'vue';
defineComponent({
name: 'GoogleOAuthChannel',
});
</script>
<template>
<OAuthChannel
provider="google"
:title="$t('INBOX_MGMT.ADD.GOOGLE.TITLE')"
:description="$t('INBOX_MGMT.ADD.GOOGLE.DESCRIPTION')"
:input-placeholder="$t('INBOX_MGMT.ADD.GOOGLE.EMAIL_PLACEHOLDER')"
:submit-button-text="$t('INBOX_MGMT.ADD.GOOGLE.SIGN_IN')"
:error-message="$t('INBOX_MGMT.ADD.GOOGLE.ERROR_MESSAGE')"
/>
</template>
@@ -1,19 +1,71 @@
<script setup>
import OAuthChannel from './OAuthChannel.vue';
import { defineComponent } from 'vue';
<template>
<div
class="border border-slate-25 dark:border-slate-800/60 bg-white dark:bg-slate-900 h-full p-6 w-full max-w-full md:w-3/4 md:max-w-[75%] flex-shrink-0 flex-grow-0"
>
<settings-sub-page-header
:header-title="$t('INBOX_MGMT.ADD.MICROSOFT.TITLE')"
:header-content="$t('INBOX_MGMT.ADD.MICROSOFT.DESCRIPTION')"
/>
<form
class="microsoft--sign-in-form"
@submit.prevent="requestAuthorization"
>
<woot-input
v-model.trim="email"
type="text"
:placeholder="$t('INBOX_MGMT.ADD.MICROSOFT.EMAIL_PLACEHOLDER')"
@blur="$v.email.$touch"
/>
<woot-submit-button
icon="brand-twitter"
type="submit"
:button-text="$t('INBOX_MGMT.ADD.MICROSOFT.SIGN_IN')"
:loading="isRequestingAuthorization"
/>
</form>
</div>
</template>
<script>
import alertMixin from 'shared/mixins/alertMixin';
import microsoftClient from 'dashboard/api/channel/microsoftClient';
import SettingsSubPageHeader from '../../../SettingsSubPageHeader.vue';
import { required, email } from 'vuelidate/lib/validators';
defineComponent({
name: 'MicrosoftOAuthChannel',
});
export default {
components: { SettingsSubPageHeader },
mixins: [alertMixin],
data() {
return { email: '', isRequestingAuthorization: false };
},
validations: {
email: { required, email },
},
methods: {
async requestAuthorization() {
try {
this.$v.$touch();
if (this.$v.$invalid) return;
this.isRequestingAuthorization = true;
const response = await microsoftClient.generateAuthorization({
email: this.email,
});
const {
data: { url },
} = response;
window.location.href = url;
} catch (error) {
this.showAlert(this.$t('INBOX_MGMT.ADD.MICROSOFT.ERROR_MESSAGE'));
} finally {
this.isRequestingAuthorization = false;
}
},
},
};
</script>
<template>
<OAuthChannel
provider="microsoft"
:title="$t('INBOX_MGMT.ADD.MICROSOFT.TITLE')"
:description="$t('INBOX_MGMT.ADD.MICROSOFT.DESCRIPTION')"
:input-placeholder="$t('INBOX_MGMT.ADD.MICROSOFT.EMAIL_PLACEHOLDER')"
:submit-button-text="$t('INBOX_MGMT.ADD.MICROSOFT.SIGN_IN')"
:error-message="$t('INBOX_MGMT.ADD.MICROSOFT.ERROR_MESSAGE')"
/>
</template>
<style scoped>
.microsoft--sign-in-form {
@apply mt-6;
}
</style>
@@ -1,89 +0,0 @@
<script setup>
import { ref, computed } from 'vue';
import microsoftClient from 'dashboard/api/channel/microsoftClient';
import googleClient from 'dashboard/api/channel/googleClient';
import SettingsSubPageHeader from '../../../SettingsSubPageHeader.vue';
import { useAlert } from 'dashboard/composables';
const props = defineProps({
provider: {
type: String,
required: true,
validate: value => ['microsoft', 'google'].includes(value),
},
title: {
type: String,
required: true,
},
description: {
type: String,
required: true,
},
submitButtonText: {
type: String,
required: true,
},
errorMessage: {
type: String,
required: true,
},
inputPlaceholder: {
type: String,
required: true,
},
});
const isRequestingAuthorization = ref(false);
const email = ref('');
const client = computed(() => {
if (props.provider === 'microsoft') {
return microsoftClient;
}
return googleClient;
});
async function requestAuthorization() {
try {
isRequestingAuthorization.value = true;
const response = await client.value.generateAuthorization({
email: email.value,
});
const {
data: { url },
} = response;
window.location.href = url;
} catch (error) {
useAlert(props.errorMessage);
} finally {
isRequestingAuthorization.value = false;
}
}
</script>
<template>
<div
class="border border-slate-25 dark:border-slate-800/60 bg-white dark:bg-slate-900 h-full p-6 w-full max-w-full md:w-3/4 md:max-w-[75%] flex-shrink-0 flex-grow-0"
>
<settings-sub-page-header
:header-title="title"
:header-content="description"
/>
<form class="mt-6" @submit.prevent="requestAuthorization">
<woot-input
v-model="email"
type="email"
:placeholder="inputPlaceholder"
/>
<woot-submit-button
icon="brand-twitter"
type="submit"
:button-text="submitButtonText"
:loading="isRequestingAuthorization"
/>
</form>
</div>
</template>
@@ -35,9 +35,7 @@ export default {
path: 'list',
name: 'settings_inbox_list',
component: InboxHome,
meta: {
permissions: ['account_manage'],
},
roles: ['administrator'],
},
{
path: 'new',
@@ -47,25 +45,19 @@ export default {
path: '',
name: 'settings_inbox_new',
component: ChannelList,
meta: {
permissions: ['account_manage'],
},
roles: ['administrator'],
},
{
path: ':inbox_id/finish',
name: 'settings_inbox_finish',
component: FinishSetup,
meta: {
permissions: ['account_manage'],
},
roles: ['administrator'],
},
{
path: ':sub_page',
name: 'settings_inboxes_page_channel',
component: channelFactory.create(),
meta: {
permissions: ['account_manage'],
},
roles: ['administrator'],
props: route => {
return { channel_name: route.params.sub_page };
},
@@ -73,9 +65,7 @@ export default {
{
path: ':inbox_id/agents',
name: 'settings_inboxes_add_agents',
meta: {
permissions: ['account_manage'],
},
roles: ['administrator'],
component: AddAgents,
},
],
@@ -84,9 +74,7 @@ export default {
path: ':inboxId',
name: 'settings_inbox_show',
component: Settings,
meta: {
permissions: ['account_manage'],
},
roles: ['administrator'],
},
],
},
@@ -26,17 +26,13 @@ export default {
path: '',
name: 'settings_applications',
component: Index,
meta: {
permissions: ['account_manage'],
},
roles: ['administrator'],
},
{
path: ':integration_id',
name: 'settings_applications_integration',
component: IntegrationHooks,
meta: {
permissions: ['account_manage'],
},
roles: ['administrator'],
props: route => ({
integrationId: route.params.integration_id,
}),
@@ -30,42 +30,32 @@ export default {
path: '',
name: 'settings_integrations',
component: Index,
meta: {
permissions: ['account_manage'],
},
roles: ['administrator'],
},
{
path: 'webhook',
component: Webhook,
name: 'settings_integrations_webhook',
meta: {
permissions: ['account_manage'],
},
roles: ['administrator'],
},
{
path: 'dashboard-apps',
component: DashboardApps,
name: 'settings_integrations_dashboard_apps',
meta: {
permissions: ['account_manage'],
},
roles: ['administrator'],
},
{
path: 'slack',
name: 'settings_integrations_slack',
component: Slack,
meta: {
permissions: ['account_manage'],
},
roles: ['administrator'],
props: route => ({ code: route.query.code }),
},
{
path: ':integration_id',
name: 'settings_integrations_integration',
component: ShowIntegration,
meta: {
permissions: ['account_manage'],
},
roles: ['administrator'],
props: route => {
return {
integrationId: route.params.integration_id,
@@ -17,17 +17,13 @@ export default {
{
path: '',
name: 'labels_wrapper',
meta: {
permissions: ['account_manage'],
},
roles: ['administrator'],
redirect: 'list',
},
{
path: 'list',
name: 'labels_list',
meta: {
permissions: ['account_manage'],
},
roles: ['administrator'],
component: Index,
},
],
@@ -23,40 +23,19 @@ export default {
path: '',
name: 'macros_wrapper',
component: Macros,
meta: {
permissions: [
'account_manage',
'conversation_manage',
'conversation_participating_manage',
'conversation_unassigned_manage',
],
},
roles: ['administrator', 'agent'],
},
{
path: 'new',
name: 'macros_new',
component: MacroEditor,
meta: {
permissions: [
'account_manage',
'conversation_manage',
'conversation_participating_manage',
'conversation_unassigned_manage',
],
},
roles: ['administrator', 'agent'],
},
{
path: ':macroId/edit',
name: 'macros_edit',
component: MacroEditor,
meta: {
permissions: [
'account_manage',
'conversation_manage',
'conversation_participating_manage',
'conversation_unassigned_manage',
],
},
roles: ['administrator', 'agent'],
},
],
},
@@ -8,28 +8,14 @@ export default {
{
path: frontendURL('accounts/:accountId/profile'),
name: 'profile_settings',
meta: {
permissions: [
'account_manage',
'conversation_manage',
'conversation_participating_manage',
'conversation_unassigned_manage',
],
},
roles: ['administrator', 'agent'],
component: SettingsContent,
children: [
{
path: 'settings',
name: 'profile_settings_index',
component: Index,
meta: {
permissions: [
'account_manage',
'conversation_manage',
'conversation_participating_manage',
'conversation_unassigned_manage',
],
},
roles: ['administrator', 'agent'],
},
],
},
@@ -29,9 +29,7 @@ export default {
{
path: 'overview',
name: 'account_overview_reports',
meta: {
permissions: ['account_manage'],
},
roles: ['administrator'],
component: LiveReports,
},
],
@@ -48,9 +46,7 @@ export default {
{
path: 'conversation',
name: 'conversation_reports',
meta: {
permissions: ['account_manage'],
},
roles: ['administrator'],
component: Index,
},
],
@@ -67,9 +63,7 @@ export default {
{
path: 'csat',
name: 'csat_reports',
meta: {
permissions: ['account_manage'],
},
roles: ['administrator'],
component: CsatResponses,
},
],
@@ -86,9 +80,7 @@ export default {
{
path: 'bot',
name: 'bot_reports',
meta: {
permissions: ['account_manage'],
},
roles: ['administrator'],
component: BotReports,
},
],
@@ -105,9 +97,7 @@ export default {
{
path: 'agent',
name: 'agent_reports',
meta: {
permissions: ['account_manage'],
},
roles: ['administrator'],
component: AgentReports,
},
],
@@ -124,9 +114,7 @@ export default {
{
path: 'label',
name: 'label_reports',
meta: {
permissions: ['account_manage'],
},
roles: ['administrator'],
component: LabelReports,
},
],
@@ -143,9 +131,7 @@ export default {
{
path: 'inboxes',
name: 'inbox_reports',
meta: {
permissions: ['account_manage'],
},
roles: ['administrator'],
component: InboxReports,
},
],
@@ -161,9 +147,7 @@ export default {
{
path: 'teams',
name: 'team_reports',
meta: {
permissions: ['account_manage'],
},
roles: ['administrator'],
component: TeamReports,
},
],
@@ -180,9 +164,7 @@ export default {
{
path: 'sla',
name: 'sla_reports',
meta: {
permissions: ['account_manage'],
},
roles: ['administrator'],
component: SLAReports,
},
],
@@ -24,14 +24,7 @@ export default {
{
path: frontendURL('accounts/:accountId/settings'),
name: 'settings_home',
meta: {
permissions: [
'account_manage',
'conversation_manage',
'conversation_participating_manage',
'conversation_unassigned_manage',
],
},
roles: ['administrator', 'agent'],
redirect: () => {
if (store.getters.getCurrentRole === 'administrator') {
return frontendURL('accounts/:accountId/settings/general');
@@ -13,17 +13,13 @@ export default {
{
path: '',
name: 'sla_wrapper',
meta: {
permissions: ['account_manage'],
},
roles: ['administrator'],
redirect: 'list',
},
{
path: 'list',
name: 'sla_list',
meta: {
permissions: ['account_manage'],
},
roles: ['administrator'],
component: Index,
},
],
@@ -36,9 +36,7 @@ export default {
path: 'list',
name: 'settings_teams_list',
component: TeamsHome,
meta: {
permissions: ['account_manage'],
},
roles: ['administrator'],
},
{
path: 'new',
@@ -48,24 +46,18 @@ export default {
path: '',
name: 'settings_teams_new',
component: CreateTeam,
meta: {
permissions: ['account_manage'],
},
roles: ['administrator'],
},
{
path: ':teamId/finish',
name: 'settings_teams_finish',
component: FinishSetup,
meta: {
permissions: ['account_manage'],
},
roles: ['administrator'],
},
{
path: ':teamId/agents',
name: 'settings_teams_add_agents',
meta: {
permissions: ['account_manage'],
},
roles: ['administrator'],
component: AddAgents,
},
],
@@ -78,24 +70,18 @@ export default {
path: '',
name: 'settings_teams_edit',
component: EditTeam,
meta: {
permissions: ['account_manage'],
},
roles: ['administrator'],
},
{
path: 'agents',
name: 'settings_teams_edit_members',
component: EditAgents,
meta: {
permissions: ['account_manage'],
},
roles: ['administrator'],
},
{
path: 'finish',
name: 'settings_teams_edit_finish',
meta: {
permissions: ['account_manage'],
},
roles: ['administrator'],
component: FinishSetup,
},
],
@@ -1,30 +1,24 @@
<script setup>
import EmptyState from 'dashboard/components/widgets/EmptyState.vue';
import { onMounted } from 'vue';
const toggleSupportWidgetVisibility = () => {
if (window.$chatwoot) {
window.$chatwoot.toggleBubbleVisibility('show');
}
};
const setupListenerForWidgetEvent = () => {
window.addEventListener('chatwoot:on-message', () => {
toggleSupportWidgetVisibility();
});
};
onMounted(() => {
toggleSupportWidgetVisibility();
setupListenerForWidgetEvent();
});
</script>
<template>
<div class="items-center bg-slate-50 flex justify-center h-full w-full">
<div class="suspended-page">
<empty-state
:title="$t('APP_GLOBAL.ACCOUNT_SUSPENDED.TITLE')"
:message="$t('APP_GLOBAL.ACCOUNT_SUSPENDED.MESSAGE')"
/>
</div>
</template>
<script>
import EmptyState from 'dashboard/components/widgets/EmptyState.vue';
export default {
components: { EmptyState },
};
</script>
<style scoped>
.suspended-page {
align-items: center;
background: var(--s-50);
display: flex;
justify-content: center;
height: 100%;
width: 100%;
}
</style>
+28 -1
View File
@@ -8,6 +8,29 @@ import AnalyticsHelper from '../helper/AnalyticsHelper';
const routes = [...dashboard.routes];
window.roleWiseRoutes = {
agent: [],
administrator: [],
};
// generateRoleWiseRoute - updates window object with agent/admin route
const generateRoleWiseRoute = route => {
route.forEach(element => {
if (element.children) {
generateRoleWiseRoute(element.children);
}
if (element.roles) {
element.roles.forEach(roleEl => {
window.roleWiseRoutes[roleEl].push(element.name);
});
}
});
};
// Create a object of routes
// accessible by each role.
// returns an object with roles as keys and routeArr as values
generateRoleWiseRoute(routes);
export const router = new VueRouter({ mode: 'history', routes });
export const validateAuthenticateRoutePermission = (to, next, { getters }) => {
@@ -22,7 +45,11 @@ export const validateAuthenticateRoutePermission = (to, next, { getters }) => {
return next(frontendURL(`accounts/${user.account_id}/dashboard`));
}
const nextRoute = validateLoggedInRoutes(to, getters.getCurrentUser);
const nextRoute = validateLoggedInRoutes(
to,
getters.getCurrentUser,
window.roleWiseRoutes
);
return nextRoute ? next(frontendURL(nextRoute)) : next();
};
-1
View File
@@ -62,7 +62,6 @@ const runSDK = ({ baseUrl, websiteToken }) => {
type: getBubbleView(chatwootSettings.type),
launcherTitle: chatwootSettings.launcherTitle || '',
showPopoutButton: chatwootSettings.showPopoutButton || false,
showUnreadMessagesDialog: chatwootSettings.showUnreadMessagesDialog ?? true,
widgetStyle: getWidgetStyle(chatwootSettings.widgetStyle) || 'standard',
resetTriggered: false,
darkMode: getDarkMode(chatwootSettings.darkMode),
+1 -2
View File
@@ -14,7 +14,6 @@ import App from '../v3/App.vue';
import router, { initalizeRouter } from '../v3/views/index';
import store from '../v3/store';
import FluentIcon from 'shared/components/FluentIcon/DashboardIcon';
import { emitter } from '../shared/helpers/mitt';
Vue.config.env = process.env;
@@ -46,11 +45,11 @@ Vue.use(VueRouter);
Vue.use(VueI18n);
Vue.use(Vuelidate);
Vue.use(AnalyticsPlugin);
Vue.prototype.$emitter = emitter;
Vue.component('fluent-icon', FluentIcon);
const i18nConfig = new VueI18n({ locale: 'en', messages: i18n });
window.bus = new Vue();
initializeChatwootEvents();
initializeAnalyticsEvents();
initalizeRouter();
-1
View File
@@ -160,7 +160,6 @@ export const IFrameHelper = {
showPopoutButton: window.$chatwoot.showPopoutButton,
widgetStyle: window.$chatwoot.widgetStyle,
darkMode: window.$chatwoot.darkMode,
showUnreadMessagesDialog: window.$chatwoot.showUnreadMessagesDialog,
campaignsSnoozedTill,
});
IFrameHelper.onLoad({
@@ -47,9 +47,6 @@ export default {
isAMicrosoftInbox() {
return this.isAnEmailChannel && this.inbox.provider === 'microsoft';
},
isAGoogleInbox() {
return this.isAnEmailChannel && this.inbox.provider === 'google';
},
isAPIInbox() {
return this.channelType === INBOX_TYPES.API;
},
@@ -1,18 +1,18 @@
<template>
<div
class="flex flex-col justify-center w-full min-h-full py-12 bg-woot-25 sm:px-6 lg:px-8 dark:bg-slate-900"
class="flex flex-col bg-woot-25 min-h-full w-full py-12 sm:px-6 lg:px-8 justify-center dark:bg-slate-900"
>
<form
class="bg-white shadow sm:mx-auto sm:w-full sm:max-w-lg dark:bg-slate-800 p-11 sm:shadow-lg sm:rounded-lg"
class="sm:mx-auto sm:w-full sm:max-w-lg bg-white dark:bg-slate-800 p-11 shadow sm:shadow-lg sm:rounded-lg"
@submit.prevent="submit"
>
<h1
class="mb-1 text-2xl font-medium tracking-tight text-left text-slate-900 dark:text-white"
class="mb-1 text-left text-2xl font-medium tracking-tight text-slate-900 dark:text-white"
>
{{ $t('RESET_PASSWORD.TITLE') }}
</h1>
<p
class="mb-4 text-sm font-normal leading-6 tracking-normal text-slate-600 dark:text-woot-50"
class="text-sm text-slate-600 dark:text-woot-50 tracking-normal font-normal leading-6 mb-4"
>
{{
useInstallationName(
@@ -36,7 +36,7 @@
:loading="resetPassword.showLoading"
/>
</div>
<p class="mt-4 -mb-1 text-sm text-slate-600 dark:text-woot-50">
<p class="text-sm text-slate-600 dark:text-woot-50 mt-4 -mb-1">
{{ $t('RESET_PASSWORD.GO_BACK_TO_LOGIN') }}
<router-link to="/auth/login" class="text-link">
{{ $t('COMMON.CLICK_HERE') }}.
@@ -101,7 +101,7 @@ export default {
if (error?.response?.data?.message) {
errorMessage = error.response.data.message;
}
this.showAlertMessage(errorMessage);
this.showAlert(errorMessage);
});
},
},
+4 -8
View File
@@ -6,16 +6,12 @@ import { validateRouteAccess } from '../helpers/RouteHelper';
export const router = new VueRouter({ mode: 'history', routes });
const sensitiveRouteNames = ['auth_password_edit'];
export const initalizeRouter = () => {
router.beforeEach((to, _, next) => {
if (!sensitiveRouteNames.includes(to.name)) {
AnalyticsHelper.page(to.name || '', {
path: to.path,
name: to.name,
});
}
AnalyticsHelper.page(to.name || '', {
path: to.path,
name: to.name,
});
return validateRouteAccess(to, next, window.chatwootConfig);
});
+2 -4
View File
@@ -214,7 +214,7 @@ export default {
login(credentials)
.then(() => {
this.showAlertMessage(this.$t('LOGIN.API.SUCCESS_MESSAGE'));
this.showAlert(this.$t('LOGIN.API.SUCCESS_MESSAGE'));
})
.catch(response => {
// Reset URL Params if the authentication is invalid
@@ -222,9 +222,7 @@ export default {
window.location = '/app/login';
}
this.loginApi.hasErrored = true;
this.showAlertMessage(
response?.message || this.$t('LOGIN.API.UNAUTH')
);
this.showAlert(response?.message || this.$t('LOGIN.API.UNAUTH'));
});
},
},
+2 -8
View File
@@ -71,7 +71,6 @@ export default {
messageCount: 'conversation/getMessageCount',
unreadMessageCount: 'conversation/getUnreadMessageCount',
isWidgetStyleFlat: 'appConfig/isWidgetStyleFlat',
showUnreadMessagesDialog: 'appConfig/getShowUnreadMessagesDialog',
}),
isIFrame() {
return IFrameHelper.isIFrame();
@@ -210,13 +209,8 @@ export default {
},
setUnreadView() {
const { unreadMessageCount } = this;
if (!this.showUnreadMessagesDialog) {
this.handleUnreadNotificationDot();
} else if (
this.isIFrame &&
unreadMessageCount > 0 &&
!this.isWidgetOpen
) {
if (this.isIFrame && unreadMessageCount > 0 && !this.isWidgetOpen) {
this.replaceRoute('unread-messages').then(() => {
this.setIframeHeight(true);
IFrameHelper.sendMessage({ event: 'setUnreadMode' });
@@ -30,7 +30,7 @@
/>
<div
v-if="hasAttachments"
class="chat-bubble has-attachment space-y-2 agent"
class="chat-bubble has-attachment agent"
:class="(wrapClass, $dm('bg-white', 'dark:bg-slate-700'))"
>
<div
@@ -44,14 +44,6 @@
:readable-time="readableTime"
@error="onImageLoadError"
/>
<video-bubble
v-if="attachment.file_type === 'video' && !hasVideoError"
:url="attachment.data_url"
:readable-time="readableTime"
@error="onVideoLoadError"
/>
<audio v-else-if="attachment.file_type === 'audio'" controls>
<source :src="attachment.data_url" />
</audio>
@@ -92,7 +84,6 @@ import AgentMessageBubble from 'widget/components/AgentMessageBubble.vue';
import MessageReplyButton from 'widget/components/MessageReplyButton.vue';
import timeMixin from 'dashboard/mixins/time';
import ImageBubble from 'widget/components/ImageBubble.vue';
import VideoBubble from 'widget/components/VideoBubble.vue';
import FileBubble from 'widget/components/FileBubble.vue';
import Thumbnail from 'dashboard/components/widgets/Thumbnail.vue';
import { MESSAGE_TYPE } from 'widget/helpers/constants';
@@ -109,7 +100,6 @@ export default {
components: {
AgentMessageBubble,
ImageBubble,
VideoBubble,
Thumbnail,
UserMessage,
FileBubble,
@@ -130,7 +120,6 @@ export default {
data() {
return {
hasImageError: false,
hasVideoError: false,
};
},
computed: {
@@ -226,20 +215,15 @@ export default {
watch: {
message() {
this.hasImageError = false;
this.hasVideoError = false;
},
},
mounted() {
this.hasImageError = false;
this.hasVideoError = false;
},
methods: {
onImageLoadError() {
this.hasImageError = true;
},
onVideoLoadError() {
this.hasVideoError = true;
},
toggleReply() {
emitter.emit(BUS_EVENTS.TOGGLE_REPLY_TO_MESSAGE, this.message);
},
@@ -39,14 +39,6 @@
:readable-time="readableTime"
@error="onImageLoadError"
/>
<video-bubble
v-if="attachment.file_type === 'video' && !hasVideoError"
:url="attachment.data_url"
:readable-time="readableTime"
@error="onVideoLoadError"
/>
<file-bubble
v-else
:url="attachment.data_url"
@@ -80,7 +72,6 @@
import UserMessageBubble from 'widget/components/UserMessageBubble.vue';
import MessageReplyButton from 'widget/components/MessageReplyButton.vue';
import ImageBubble from 'widget/components/ImageBubble.vue';
import VideoBubble from 'widget/components/VideoBubble.vue';
import FluentIcon from 'shared/components/FluentIcon/Index.vue';
import FileBubble from 'widget/components/FileBubble.vue';
import timeMixin from 'dashboard/mixins/time';
@@ -97,7 +88,6 @@ export default {
UserMessageBubble,
MessageReplyButton,
ImageBubble,
VideoBubble,
FileBubble,
FluentIcon,
ReplyToChip,
@@ -117,7 +107,6 @@ export default {
data() {
return {
hasImageError: false,
hasVideoError: false,
};
},
computed: {
@@ -154,12 +143,10 @@ export default {
watch: {
message() {
this.hasImageError = false;
this.hasVideoError = false;
},
},
mounted() {
this.hasImageError = false;
this.hasVideoError = false;
},
methods: {
async retrySendMessage() {
@@ -171,9 +158,6 @@ export default {
onImageLoadError() {
this.hasImageError = true;
},
onVideoLoadError() {
this.hasVideoError = true;
},
toggleReply() {
this.$emitter.emit(BUS_EVENTS.TOGGLE_REPLY_TO_MESSAGE, this.message);
},
@@ -1,27 +0,0 @@
<script setup>
defineProps({
url: { type: String, default: '' },
readableTime: { type: String, default: '' },
});
const emits = defineEmits(['error']);
const onVideoError = () => {
emits('error');
};
</script>
<template>
<div class="block relative max-w-full">
<video
class="w-full max-w-[250px] h-auto"
:src="url"
controls
@error="onVideoError"
/>
<span
class="text-xs absolute text-white dark:text-white right-3 bottom-1 whitespace-nowrap"
>
{{ readableTime }}
</span>
</div>
</template>
@@ -10,7 +10,6 @@ import {
const state = {
hideMessageBubble: false,
isCampaignViewClicked: false,
showUnreadMessagesDialog: true,
isWebWidgetTriggered: false,
isWidgetOpen: false,
position: 'right',
@@ -30,7 +29,6 @@ export const getters = {
getReferrerHost: $state => $state.referrerHost,
isWidgetStyleFlat: $state => $state.widgetStyle === 'flat',
darkMode: $state => $state.darkMode,
getShowUnreadMessagesDialog: $state => $state.showUnreadMessagesDialog,
};
export const actions = {
@@ -40,7 +38,6 @@ export const actions = {
showPopoutButton,
position,
hideMessageBubble,
showUnreadMessagesDialog,
widgetStyle = 'rounded',
darkMode = 'light',
}
@@ -49,7 +46,6 @@ export const actions = {
hideMessageBubble: !!hideMessageBubble,
position: position || 'right',
showPopoutButton: !!showPopoutButton,
showUnreadMessagesDialog: !!showUnreadMessagesDialog,
widgetStyle,
darkMode,
});
@@ -79,7 +75,6 @@ export const mutations = {
$state.widgetStyle = data.widgetStyle;
$state.darkMode = data.darkMode;
$state.locale = data.locale || $state.locale;
$state.showUnreadMessagesDialog = data.showUnreadMessagesDialog;
},
[TOGGLE_WIDGET_OPEN]($state, isWidgetOpen) {
$state.isWidgetOpen = isWidgetOpen;
@@ -13,10 +13,4 @@ describe('#getters', () => {
expect(getters.getReferrerHost(state)).toEqual('www.chatwoot.com');
});
});
describe('#getShowUnreadMessagesDialog', () => {
it('returns correct value', () => {
const state = { showUnreadMessagesDialog: true };
expect(getters.getShowUnreadMessagesDialog(state)).toEqual(true);
});
});
});
@@ -30,8 +30,6 @@ class Inboxes::FetchImapEmailsJob < MutexApplicationJob
def process_email_for_channel(channel, interval)
inbound_emails = if channel.microsoft?
Imap::MicrosoftFetchEmailService.new(channel: channel, interval: interval).perform
elsif channel.google?
Imap::GoogleFetchEmailService.new(channel: channel, interval: interval).perform
else
Imap::FetchEmailService.new(channel: channel, interval: interval).perform
end
+1 -1
View File
@@ -34,7 +34,7 @@ class ReportingEventListener < BaseListener
message.created_at),
account_id: conversation.account_id,
inbox_id: conversation.inbox_id,
user_id: message.sender_id,
user_id: conversation.assignee_id,
conversation_id: conversation.id,
event_start_time: last_non_human_activity(conversation),
event_end_time: message.created_at
+16 -4
View File
@@ -1,6 +1,5 @@
class Imap::ImapMailbox
include MailboxHelper
include IncomingEmailValidityHelper
attr_accessor :channel, :account, :inbox, :conversation, :processed_mail
def process(mail, channel)
@@ -10,10 +9,11 @@ class Imap::ImapMailbox
load_inbox
decorate_mail
Rails.logger.info("Processing Email from: #{@processed_mail.original_sender} : inbox #{@inbox.id} : message_id #{@processed_mail.message_id}")
# prevent loop from chatwoot notification emails
return if notification_email_from_chatwoot?
# Skip processing email if it belongs to any of the edge cases
return unless incoming_email_from_valid_email?
# Stop processing if email format doesn't match Chatwoot supported mail format
return unless email_from_valid_email?
ActiveRecord::Base.transaction do
find_or_create_contact
@@ -37,6 +37,18 @@ class Imap::ImapMailbox
@processed_mail = MailPresenter.new(@inbound_mail, @account)
end
def email_from_valid_email?
Rails.logger.info("Processing Email from: #{@processed_mail.original_sender} : inbox #{@inbox.id}")
# validate email with Devise.email_regexp
if Devise.email_regexp.match?(@processed_mail.original_sender)
true
else
Rails.logger.error("Email from: #{@processed_mail.original_sender} : inbox #{@inbox.id} is invalid")
false
end
end
def find_conversation_by_in_reply_to
return if in_reply_to.blank?
@@ -1,35 +0,0 @@
module IncomingEmailValidityHelper
private
def incoming_email_from_valid_email?
return false unless valid_external_email_for_active_account?
# we skip processing auto reply emails like delivery status notifications
# out of office replies, etc.
return false if auto_reply_email?
# return if email doesn't have a valid sender
# This can happen in cases like bounce emails for invalid contact email address
# TODO: Handle the bounce separately and mark the contact as invalid in case of reply bounces
# The returned value could be "\"\"" for some email clients
return false unless Devise.email_regexp.match?(@processed_mail.original_sender)
true
end
def valid_external_email_for_active_account?
return false unless @account.active?
return false if @processed_mail.notification_email_from_chatwoot?
true
end
def auto_reply_email?
if @processed_mail.auto_reply?
Rails.logger.info "is_auto_reply? : #{processed_mail.auto_reply?}"
true
else
false
end
end
end
+5
View File
@@ -112,6 +112,11 @@ module MailboxHelper
Rails.logger.info "[MailboxHelper] Contact created with ID: #{@contact.id} for inbox with ID: #{@inbox.id}"
end
def notification_email_from_chatwoot?
# notification emails are send via mailer sender email address. so it should match
@processed_mail.original_sender == Mail::Address.new(ENV.fetch('MAILER_SENDER_EMAIL', 'Chatwoot <accounts@chatwoot.com>')).address
end
def mail_content
if processed_mail.text_content.present?
processed_mail.text_content[:reply]
+10 -3
View File
@@ -1,5 +1,4 @@
class SupportMailbox < ApplicationMailbox
include IncomingEmailValidityHelper
attr_accessor :channel, :account, :inbox, :conversation, :processed_mail
before_processing :find_channel,
@@ -10,8 +9,16 @@ class SupportMailbox < ApplicationMailbox
def process
Rails.logger.info "Processing email #{mail.message_id} from #{original_sender_email} to #{mail.to} with subject #{mail.subject}"
# Skip processing email if it belongs to any of the edge cases
return unless incoming_email_from_valid_email?
# to turn off spam conversation creation
return unless @account.active?
# prevent loop from chatwoot notification emails
return if notification_email_from_chatwoot?
# return if email doesn't have a valid sender
# This can happen in cases like bounce emails for invalid contact email address
# TODO: Handle the bounce seperately and mark the contact as invalid
# we are checking for @ since the returned value could be "\"\"" for some email clients
return unless original_sender_email.include?('@')
ActiveRecord::Base.transaction do
find_or_create_contact
+10 -24
View File
@@ -14,7 +14,6 @@ module ConversationReplyMailerHelper
@options[:bcc] = cc_bcc_emails[1]
end
ms_smtp_settings
google_smtp_settings
set_delivery_method
Rails.logger.info("Email sent from #{email_from} to #{to_emails} with subject #{mail_subject}")
@@ -24,36 +23,23 @@ module ConversationReplyMailerHelper
private
def google_smtp_settings
return unless @inbox.email? && @channel.imap_enabled && @inbox.channel.google?
smtp_settings = base_smtp_settings('smtp.gmail.com')
@options[:delivery_method] = :smtp
@options[:delivery_method_options] = smtp_settings
end
def ms_smtp_settings
return unless @inbox.email? && @channel.imap_enabled && @inbox.channel.microsoft?
return unless @inbox.email? && @channel.imap_enabled && @inbox.channel.provider == 'microsoft'
smtp_settings = base_smtp_settings('smtp.office365.com')
@options[:delivery_method] = :smtp
@options[:delivery_method_options] = smtp_settings
end
def base_smtp_settings(domain)
{
address: domain,
smtp_settings = {
address: 'smtp.office365.com',
port: 587,
user_name: @channel.imap_login,
password: @channel.provider_config['access_token'],
domain: domain,
domain: 'smtp.office365.com',
tls: false,
enable_starttls_auto: true,
openssl_verify_mode: 'none',
authentication: 'xoauth2'
}
@options[:delivery_method] = :smtp
@options[:delivery_method_options] = smtp_settings
end
def set_delivery_method
@@ -83,12 +69,12 @@ module ConversationReplyMailerHelper
@inbox.inbox_type == 'Email' && @channel.imap_enabled
end
def email_oauth_enabled
@inbox.inbox_type == 'Email' && (@channel.microsoft? || @channel.google?)
def email_microsoft_auth_enabled
@inbox.inbox_type == 'Email' && @channel.provider == 'microsoft'
end
def email_from
email_oauth_enabled || email_smtp_enabled ? channel_email_with_name : from_email_with_name
email_microsoft_auth_enabled || email_smtp_enabled ? channel_email_with_name : from_email_with_name
end
def email_reply_to
-8
View File
@@ -49,14 +49,6 @@ class AccountUser < ApplicationRecord
::Agents::DestroyJob.perform_later(account, user)
end
def permissions
if administrator?
GlobalConfigService.load('CHATWOOT_ADMIN_PERMISSIONS', 'account_manage').split(',')
else
GlobalConfigService.load('CHATWOOT_AGENT_PERMISSIONS', 'conversation_manage').split(',')
end
end
def push_event_data
{
id: id,
-4
View File
@@ -57,10 +57,6 @@ class Channel::Email < ApplicationRecord
provider == 'microsoft'
end
def google?
provider == 'google'
end
private
def ensure_forward_to_email
@@ -48,6 +48,7 @@ class JsonSchemaValidator < ActiveModel::Validator
# Add validation errors to the record with a formatted statement
validation_errors.each do |error|
# byebug
format_and_append_error(error, record)
end
end
-17
View File
@@ -146,25 +146,8 @@ class MailPresenter < SimpleDelegator
end
end
def auto_reply?
auto_submitted? || x_auto_reply?
end
def notification_email_from_chatwoot?
# notification emails are send via mailer sender email address. so it should match
original_sender == Mail::Address.new(ENV.fetch('MAILER_SENDER_EMAIL', 'Chatwoot <accounts@chatwoot.com>')).address
end
private
def auto_submitted?
@mail['Auto-Submitted'].present? && @mail['Auto-Submitted'].value != 'no'
end
def x_auto_reply?
@mail['X-Autoreply'].present? && @mail['X-Autoreply'].value == 'yes'
end
# forcing the encoding of the content to UTF-8 so as to be compatible with database and serializers
def encode_to_unicode(str)
return '' if str.blank?
+1 -5
View File
@@ -50,11 +50,7 @@ class ActionService
end
def assign_team(team_ids = [])
# FIXME: The explicit checks for zero or nil (string) is bad. Move
# this to a separate unassign action.
should_unassign = team_ids.blank? || %w[nil 0].include?(team_ids[0].to_s)
return @conversation.update!(team_id: nil) if should_unassign
return unassign_team if team_ids[0]&.zero?
# check if team belongs to account only if team_id is present
# if team_id is nil, then it means that the team is being unassigned
return unless !team_ids[0].nil? && team_belongs_to_account?(team_ids)
@@ -23,7 +23,7 @@ class AutoAssignment::InboxRoundRobinService
# end of queue management functions
# allowed member ids = [assignable online agents supplied by the assignment service]
# allowed member ids = [assignable online agents supplied by the assignement service]
# the values of allowed member ids should be in string format
def available_agent(allowed_agent_ids: [])
reset_queue unless validate_queue?
@@ -1,62 +0,0 @@
class BaseRefreshOauthTokenService
pattr_initialize [:channel!]
# Additional references: https://gitlab.com/gitlab-org/ruby/gems/gitlab-mail_room/-/blob/master/lib/mail_room/microsoft_graph/connection.rb
def access_token
return provider_config[:access_token] unless access_token_expired?
refreshed_tokens = refresh_tokens
refreshed_tokens[:access_token]
end
def access_token_expired?
expiry = provider_config[:expires_on]
return true if expiry.blank?
# Adding a 5 minute window to expiry check to avoid any race
# conditions during the fetch operation. This would assure that the
# tokens are updated when we fetch the emails.
Time.current.utc >= DateTime.parse(expiry) - 5.minutes
end
# Refresh the access tokens using the refresh token
# Refer: https://github.com/microsoftgraph/msgraph-sample-rubyrailsapp/tree/b4a6869fe4a438cde42b161196484a929f1bee46
def refresh_tokens
oauth_strategy = build_oauth_strategy
token_service = build_token_service(oauth_strategy)
new_tokens = token_service.refresh!.to_hash.slice(:access_token, :refresh_token, :expires_at)
update_channel_provider_config(new_tokens)
channel.reload.provider_config
end
def update_channel_provider_config(new_tokens)
channel.provider_config = {
access_token: new_tokens[:access_token],
refresh_token: new_tokens[:refresh_token],
expires_on: Time.at(new_tokens[:expires_at]).utc.to_s
}
channel.save!
end
private
def build_oauth_strategy
raise NotImplementedError
end
def provider_config
@provider_config ||= channel.provider_config.with_indifferent_access
end
# Builds the token service using OAuth2
def build_token_service(oauth_strategy)
OAuth2::AccessToken.new(
oauth_strategy.client,
provider_config[:access_token],
refresh_token: provider_config[:refresh_token]
)
end
end
@@ -1,12 +0,0 @@
# Refer: https://learn.microsoft.com/en-us/entra/identity-platform/configurable-token-lifetimes
class Google::RefreshOauthTokenService < BaseRefreshOauthTokenService
private
# Builds the OAuth strategy for Microsoft Graph
def build_oauth_strategy
app_id = GlobalConfigService.load('GOOGLE_OAUTH_CLIENT_ID', nil)
app_secret = GlobalConfigService.load('GOOGLE_OAUTH_CLIENT_SECRET', nil)
OmniAuth::Strategies::GoogleOauth2.new(nil, app_id, app_secret)
end
end
@@ -1,17 +0,0 @@
class Imap::GoogleFetchEmailService < Imap::BaseFetchEmailService
def fetch_emails
return if channel.provider_config['access_token'].blank?
fetch_mail_for_channel
end
private
def authentication_type
'XOAUTH2'
end
def imap_password
Google::RefreshOauthTokenService.new(channel: channel).access_token
end
end
@@ -1,9 +1,64 @@
# Refer: https://learn.microsoft.com/en-us/entra/identity-platform/configurable-token-lifetimes
class Microsoft::RefreshOauthTokenService < BaseRefreshOauthTokenService
class Microsoft::RefreshOauthTokenService
pattr_initialize [:channel!]
# Additional references: https://gitlab.com/gitlab-org/ruby/gems/gitlab-mail_room/-/blob/master/lib/mail_room/microsoft_graph/connection.rb
def access_token
return provider_config[:access_token] unless access_token_expired?
refreshed_tokens = refresh_tokens
refreshed_tokens[:access_token]
end
def access_token_expired?
expiry = provider_config[:expires_on]
return true if expiry.blank?
# Adding a 5 minute window to expiry check to avoid any race
# conditions during the fetch operation. This would assure that the
# tokens are updated when we fetch the emails.
Time.current.utc >= DateTime.parse(expiry) - 5.minutes
end
# Refresh the access tokens using the refresh token
# Refer: https://github.com/microsoftgraph/msgraph-sample-rubyrailsapp/tree/b4a6869fe4a438cde42b161196484a929f1bee46
def refresh_tokens
oauth_strategy = build_oauth_strategy
token_service = build_token_service(oauth_strategy)
new_tokens = token_service.refresh!.to_hash.slice(:access_token, :refresh_token, :expires_at)
update_channel_provider_config(new_tokens)
channel.reload.provider_config
end
def update_channel_provider_config(new_tokens)
channel.provider_config = {
access_token: new_tokens[:access_token],
refresh_token: new_tokens[:refresh_token],
expires_on: Time.at(new_tokens[:expires_at]).utc.to_s
}
channel.save!
end
private
def provider_config
@provider_config ||= channel.provider_config.with_indifferent_access
end
# Builds the OAuth strategy for Microsoft Graph
def build_oauth_strategy
::MicrosoftGraphAuth.new(nil, GlobalConfigService.load('AZURE_APP_ID', ''), GlobalConfigService.load('AZURE_APP_SECRET', ''))
end
# Builds the token service using OAuth2
def build_token_service(oauth_strategy)
OAuth2::AccessToken.new(
oauth_strategy.client,
provider_config[:access_token],
refresh_token: provider_config[:refresh_token]
)
end
end
+8 -3
View File
@@ -1,4 +1,4 @@
class Notification::FcmService
class FCMService
SCOPES = ['https://www.googleapis.com/auth/firebase.messaging'].freeze
def initialize(project_id, credentials)
@@ -24,7 +24,7 @@ class Notification::FcmService
def generate_token
authorizer = Google::Auth::ServiceAccountCredentials.make_creds(
json_key_io: credentials_path,
json_key_io: File.open(credentials_path),
scope: SCOPES
)
token = authorizer.fetch_access_token!
@@ -35,6 +35,11 @@ class Notification::FcmService
end
def credentials_path
StringIO.new(@credentials)
file = Tempfile.new(['firebase', '.json'])
file.write(@credentials)
file.rewind
file.path
ensure
file.close
end
end

Some files were not shown because too many files have changed in this diff Show More