Compare commits
4
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
e622375b23 | ||
|
|
410eeeb610 | ||
|
|
343f7ed2d3 | ||
|
|
61eaa96b85 |
@@ -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
@@ -75,4 +75,5 @@ yalc.lock
|
||||
yarn-debug.log*
|
||||
.yarn-integrity
|
||||
|
||||
/storybook-static
|
||||
/storybook-static
|
||||
config/chatwoot-fcm-service-account-file.json
|
||||
@@ -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
@@ -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
@@ -1 +1 @@
|
||||
3.9.0
|
||||
3.3.1
|
||||
|
||||
+1
-1
@@ -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
|
||||
|
||||
@@ -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"
|
||||
|
||||
-73
@@ -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>
|
||||
|
||||
-18
@@ -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>
|
||||
+68
-16
@@ -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>
|
||||
|
||||
-89
@@ -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'],
|
||||
},
|
||||
],
|
||||
},
|
||||
|
||||
+2
-6
@@ -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,
|
||||
}),
|
||||
|
||||
+5
-15
@@ -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>
|
||||
|
||||
@@ -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();
|
||||
};
|
||||
|
||||
|
||||
@@ -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),
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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);
|
||||
});
|
||||
},
|
||||
},
|
||||
|
||||
@@ -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);
|
||||
});
|
||||
|
||||
@@ -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'));
|
||||
});
|
||||
},
|
||||
},
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
@@ -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]
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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?
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
Reference in New Issue
Block a user