From 05ea6308f2bbff2c6cc92608d508b1deb022e4b6 Mon Sep 17 00:00:00 2001 From: Sojan Jose Date: Fri, 8 May 2020 12:13:23 +0530 Subject: [PATCH 01/42] Chore: Update ruby and rails versions (#831) --- .circleci/config.yml | 2 +- .ruby-version | 2 +- Gemfile | 2 +- Gemfile.lock | 154 +++++++++--------- app/jobs/action_cable_broadcast_job.rb | 2 +- app/models/conversation.rb | 2 +- config/environments/production.rb | 2 +- config/environments/staging.rb | 2 +- deployment/chatwoot-web.1.service | 6 +- deployment/chatwoot-worker.1.service | 6 +- deployment/setup.sh | 4 +- docker/Dockerfile | 8 +- docs/development/environment-setup/mac-os.md | 8 +- docs/development/environment-setup/ubuntu.md | 8 +- docs/development/environment-setup/windows.md | 6 +- 15 files changed, 107 insertions(+), 107 deletions(-) diff --git a/.circleci/config.yml b/.circleci/config.yml index 7b72fb0c0..9579c23a8 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -7,7 +7,7 @@ defaults: &defaults working_directory: ~/build docker: # specify the version you desire here - - image: circleci/ruby:2.7.0-node-browsers + - image: circleci/ruby:2.7.1-node-browsers # Specify service dependencies here if necessary # CircleCI maintains a library of pre-built images diff --git a/.ruby-version b/.ruby-version index 24ba9a38d..860487ca1 100644 --- a/.ruby-version +++ b/.ruby-version @@ -1 +1 @@ -2.7.0 +2.7.1 diff --git a/Gemfile b/Gemfile index e8e2fa85b..61b3f4bcd 100644 --- a/Gemfile +++ b/Gemfile @@ -1,6 +1,6 @@ source 'https://rubygems.org' -ruby '2.7.0' +ruby '2.7.1' ##-- base gems for rails --## gem 'rack-cors', require: 'rack/cors' diff --git a/Gemfile.lock b/Gemfile.lock index f26503fa4..1932d7e72 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -25,61 +25,61 @@ GEM specs: action-cable-testing (0.6.1) actioncable (>= 5.0) - actioncable (6.0.2.2) - actionpack (= 6.0.2.2) + actioncable (6.0.3) + actionpack (= 6.0.3) nio4r (~> 2.0) websocket-driver (>= 0.6.1) - actionmailbox (6.0.2.2) - actionpack (= 6.0.2.2) - activejob (= 6.0.2.2) - activerecord (= 6.0.2.2) - activestorage (= 6.0.2.2) - activesupport (= 6.0.2.2) + actionmailbox (6.0.3) + actionpack (= 6.0.3) + activejob (= 6.0.3) + activerecord (= 6.0.3) + activestorage (= 6.0.3) + activesupport (= 6.0.3) mail (>= 2.7.1) - actionmailer (6.0.2.2) - actionpack (= 6.0.2.2) - actionview (= 6.0.2.2) - activejob (= 6.0.2.2) + actionmailer (6.0.3) + actionpack (= 6.0.3) + actionview (= 6.0.3) + activejob (= 6.0.3) mail (~> 2.5, >= 2.5.4) rails-dom-testing (~> 2.0) - actionpack (6.0.2.2) - actionview (= 6.0.2.2) - activesupport (= 6.0.2.2) + actionpack (6.0.3) + actionview (= 6.0.3) + activesupport (= 6.0.3) rack (~> 2.0, >= 2.0.8) rack-test (>= 0.6.3) rails-dom-testing (~> 2.0) rails-html-sanitizer (~> 1.0, >= 1.2.0) - actiontext (6.0.2.2) - actionpack (= 6.0.2.2) - activerecord (= 6.0.2.2) - activestorage (= 6.0.2.2) - activesupport (= 6.0.2.2) + actiontext (6.0.3) + actionpack (= 6.0.3) + activerecord (= 6.0.3) + activestorage (= 6.0.3) + activesupport (= 6.0.3) nokogiri (>= 1.8.5) - actionview (6.0.2.2) - activesupport (= 6.0.2.2) + actionview (6.0.3) + activesupport (= 6.0.3) builder (~> 3.1) erubi (~> 1.4) rails-dom-testing (~> 2.0) rails-html-sanitizer (~> 1.1, >= 1.2.0) - activejob (6.0.2.2) - activesupport (= 6.0.2.2) + activejob (6.0.3) + activesupport (= 6.0.3) globalid (>= 0.3.6) - activemodel (6.0.2.2) - activesupport (= 6.0.2.2) - activerecord (6.0.2.2) - activemodel (= 6.0.2.2) - activesupport (= 6.0.2.2) - activestorage (6.0.2.2) - actionpack (= 6.0.2.2) - activejob (= 6.0.2.2) - activerecord (= 6.0.2.2) + activemodel (6.0.3) + activesupport (= 6.0.3) + activerecord (6.0.3) + activemodel (= 6.0.3) + activesupport (= 6.0.3) + activestorage (6.0.3) + actionpack (= 6.0.3) + activejob (= 6.0.3) + activerecord (= 6.0.3) marcel (~> 0.3.1) - activesupport (6.0.2.2) + activesupport (6.0.3) concurrent-ruby (~> 1.0, >= 1.0.2) i18n (>= 0.7, < 2) minitest (~> 5.1) tzinfo (~> 1.1) - zeitwerk (~> 2.2) + zeitwerk (~> 2.2, >= 2.2.2) acts-as-taggable-on (6.5.0) activerecord (>= 5.0, < 6.1) addressable (2.7.0) @@ -90,8 +90,8 @@ GEM ast (2.4.0) attr_extras (6.2.3) aws-eventstream (1.1.0) - aws-partitions (1.296.0) - aws-sdk-core (3.94.0) + aws-partitions (1.310.0) + aws-sdk-core (3.94.1) aws-eventstream (~> 1, >= 1.0.2) aws-partitions (~> 1, >= 1.239.0) aws-sigv4 (~> 1.1) @@ -99,11 +99,11 @@ GEM aws-sdk-kms (1.30.0) aws-sdk-core (~> 3, >= 3.71.0) aws-sigv4 (~> 1.1) - aws-sdk-s3 (1.61.2) + aws-sdk-s3 (1.63.1) aws-sdk-core (~> 3, >= 3.83.0) aws-sdk-kms (~> 1) aws-sigv4 (~> 1.1) - aws-sigv4 (1.1.1) + aws-sigv4 (1.1.3) aws-eventstream (~> 1.0, >= 1.0.2) axiom-types (0.1.1) descendants_tracker (~> 0.0.4) @@ -131,7 +131,7 @@ GEM bundler-audit (0.6.1) bundler (>= 1.2.0, < 3) thor (~> 0.18) - byebug (11.1.1) + byebug (11.1.3) chargebee (2.7.5) json_pure (~> 2.1) rest-client (>= 1.8, < 3.0) @@ -167,13 +167,13 @@ GEM equalizer (0.0.11) erubi (1.9.0) execjs (2.7.0) - facebook-messenger (1.4.1) + facebook-messenger (1.5.0) httparty (~> 0.13, >= 0.13.7) rack (>= 1.4.5) - factory_bot (5.1.2) + factory_bot (5.2.0) activesupport (>= 4.2.0) - factory_bot_rails (5.1.1) - factory_bot (~> 5.1.0) + factory_bot_rails (5.2.0) + factory_bot (~> 5.2.0) railties (>= 4.2.0) faker (2.11.0) i18n (>= 1.6, < 2) @@ -217,7 +217,7 @@ GEM groupdate (5.0.0) activesupport (>= 5) haikunator (1.1.0) - hana (1.3.5) + hana (1.3.6) hashie (4.1.0) hkdf (0.3.0) http-accept (1.7.0) @@ -272,8 +272,8 @@ GEM method_source (1.0.0) mime-types (3.3.1) mime-types-data (~> 3.2015) - mime-types-data (3.2019.1009) - mimemagic (0.3.4) + mime-types-data (3.2020.0425) + mimemagic (0.3.5) mini_magick (4.10.1) mini_mime (1.0.2) mini_portile2 (2.4.0) @@ -290,7 +290,7 @@ GEM orm_adapter (0.5.0) os (1.1.0) parallel (1.19.1) - parser (2.7.1.1) + parser (2.7.1.2) ast (~> 2.4.0) pg (1.2.3) pry (0.13.1) @@ -314,38 +314,38 @@ GEM rack rack-test (1.1.0) rack (>= 1.0, < 3) - rails (6.0.2.2) - actioncable (= 6.0.2.2) - actionmailbox (= 6.0.2.2) - actionmailer (= 6.0.2.2) - actionpack (= 6.0.2.2) - actiontext (= 6.0.2.2) - actionview (= 6.0.2.2) - activejob (= 6.0.2.2) - activemodel (= 6.0.2.2) - activerecord (= 6.0.2.2) - activestorage (= 6.0.2.2) - activesupport (= 6.0.2.2) + rails (6.0.3) + actioncable (= 6.0.3) + actionmailbox (= 6.0.3) + actionmailer (= 6.0.3) + actionpack (= 6.0.3) + actiontext (= 6.0.3) + actionview (= 6.0.3) + activejob (= 6.0.3) + activemodel (= 6.0.3) + activerecord (= 6.0.3) + activestorage (= 6.0.3) + activesupport (= 6.0.3) bundler (>= 1.3.0) - railties (= 6.0.2.2) + railties (= 6.0.3) sprockets-rails (>= 2.0.0) rails-dom-testing (2.0.3) activesupport (>= 4.2.0) nokogiri (>= 1.6) rails-html-sanitizer (1.3.0) loofah (~> 2.3) - railties (6.0.2.2) - actionpack (= 6.0.2.2) - activesupport (= 6.0.2.2) + railties (6.0.3) + actionpack (= 6.0.3) + activesupport (= 6.0.3) method_source rake (>= 0.8.7) thor (>= 0.20.3, < 2.0) rainbow (3.0.0) rake (13.0.1) - rb-fsevent (0.10.3) + rb-fsevent (0.10.4) rb-inotify (0.10.1) ffi (~> 1.0) - redis (4.1.3) + redis (4.1.4) redis-namespace (1.7.0) redis (>= 3.0.4) redis-rack-cache (2.2.1) @@ -367,8 +367,8 @@ GEM netrc (~> 0.8) retriable (3.1.2) rexml (3.2.4) - rspec-core (3.9.1) - rspec-support (~> 3.9.1) + rspec-core (3.9.2) + rspec-support (~> 3.9.3) rspec-expectations (3.9.1) diff-lcs (>= 1.2.0, < 2.0) rspec-support (~> 3.9.0) @@ -383,8 +383,8 @@ GEM rspec-expectations (~> 3.9) rspec-mocks (~> 3.9) rspec-support (~> 3.9) - rspec-support (3.9.2) - rubocop (0.81.0) + rspec-support (3.9.3) + rubocop (0.82.0) jaro_winkler (~> 1.5.1) parallel (~> 1.10) parser (>= 2.7.0.1) @@ -398,7 +398,7 @@ GEM activesupport rack (>= 1.1) rubocop (>= 0.72.0) - rubocop-rspec (1.38.1) + rubocop-rspec (1.39.0) rubocop (>= 0.68.1) ruby-progressbar (1.10.1) sass (3.7.4) @@ -418,7 +418,7 @@ GEM faraday (>= 1.0) shoulda-matchers (4.3.0) activesupport (>= 4.2.0) - sidekiq (6.0.6) + sidekiq (6.0.7) connection_pool (>= 2.2.2) rack (~> 2.0) rack-protection (>= 2.0.0) @@ -460,7 +460,7 @@ GEM nokogiri (>= 1.6, < 2.0) tzinfo (1.2.7) thread_safe (~> 0.1) - tzinfo-data (1.2019.3) + tzinfo-data (1.2020.1) tzinfo (>= 1.0.0) uber (0.1.0) uglifier (4.2.0) @@ -480,12 +480,12 @@ GEM equalizer (~> 0.0, >= 0.0.9) warden (1.2.8) rack (>= 2.0.6) - web-console (4.0.1) + web-console (4.0.2) actionview (>= 6.0.0) activemodel (>= 6.0.0) bindex (>= 0.4.0) railties (>= 6.0.0) - webpacker (5.0.1) + webpacker (5.1.1) activesupport (>= 5.2) rack-proxy (>= 0.6.1) railties (>= 5.2) @@ -576,7 +576,7 @@ DEPENDENCIES wisper (= 2.0.0) RUBY VERSION - ruby 2.7.0p0 + ruby 2.7.1p83 BUNDLED WITH - 2.1.2 + 2.1.4 diff --git a/app/jobs/action_cable_broadcast_job.rb b/app/jobs/action_cable_broadcast_job.rb index 5199cc771..aedae2c46 100644 --- a/app/jobs/action_cable_broadcast_job.rb +++ b/app/jobs/action_cable_broadcast_job.rb @@ -3,7 +3,7 @@ class ActionCableBroadcastJob < ApplicationJob def perform(members, event_name, data) members.each do |member| - ActionCable.server.broadcast(member, event: event_name, data: data) + ActionCable.server.broadcast(member, { event: event_name, data: data }) end end end diff --git a/app/models/conversation.rb b/app/models/conversation.rb index a6cc6b68e..d72202a98 100644 --- a/app/models/conversation.rb +++ b/app/models/conversation.rb @@ -180,7 +180,7 @@ class Conversation < ApplicationRecord def create_assignee_change(user_name) params = { assignee_name: assignee&.name, user_name: user_name }.compact key = assignee_id ? 'assigned' : 'removed' - content = I18n.t("conversations.activity.assignee.#{key}", params) + content = I18n.t("conversations.activity.assignee.#{key}", **params) messages.create(activity_message_params(content)) end diff --git a/config/environments/production.rb b/config/environments/production.rb index 65f609e49..b796d6acd 100644 --- a/config/environments/production.rb +++ b/config/environments/production.rb @@ -66,7 +66,7 @@ Rails.application.configure do # Enable locale fallbacks for I18n (makes lookups for any locale fall back to # the I18n.default_locale when a translation cannot be found). - config.i18n.fallbacks = true + config.i18n.fallbacks = [I18n.default_locale] # Send deprecation notices to registered listeners. config.active_support.deprecation = :notify diff --git a/config/environments/staging.rb b/config/environments/staging.rb index 35746997d..585f35da4 100644 --- a/config/environments/staging.rb +++ b/config/environments/staging.rb @@ -81,7 +81,7 @@ Rails.application.configure do # Enable locale fallbacks for I18n (makes lookups for any locale fall back to # the I18n.default_locale when a translation cannot be found). - config.i18n.fallbacks = true + config.i18n.fallbacks = [I18n.default_locale] config.active_job.queue_adapter = :sidekiq # Send deprecation notices to registered listeners. diff --git a/deployment/chatwoot-web.1.service b/deployment/chatwoot-web.1.service index 5885f618a..56d6a283c 100644 --- a/deployment/chatwoot-web.1.service +++ b/deployment/chatwoot-web.1.service @@ -16,10 +16,10 @@ KillMode=mixed StandardInput=null SyslogIdentifier=%p -Environment="PATH=/home/chatwoot/.rvm/gems/ruby-2.7.0/bin:/home/chatwoot/.rvm/gems/ruby-2.7.0@global/bin:/home/chatwoot/.rvm/rubies/ruby-2.7.0/bin:/home/chatwoot/.rvm/bin:/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin:/snap/bin:/home/chatwoot/.rvm/bin:/home/chatwoot/.rvm/bin" +Environment="PATH=/home/chatwoot/.rvm/gems/ruby-2.7.1/bin:/home/chatwoot/.rvm/gems/ruby-2.7.1@global/bin:/home/chatwoot/.rvm/rubies/ruby-2.7.1/bin:/home/chatwoot/.rvm/bin:/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin:/snap/bin:/home/chatwoot/.rvm/bin:/home/chatwoot/.rvm/bin" Environment="PORT=3000" Environment="RAILS_ENV=production" Environment="NODE_ENV=production" Environment="RAILS_LOG_TO_STDOUT=true" -Environment="GEM_HOME=/home/chatwoot/.rvm/gems/ruby-2.7.0" -Environment="GEM_PATH=/home/chatwoot/.rvm/gems/ruby-2.7.0:/home/chatwoot/.rvm/gems/ruby-2.7.0@global" \ No newline at end of file +Environment="GEM_HOME=/home/chatwoot/.rvm/gems/ruby-2.7.1" +Environment="GEM_PATH=/home/chatwoot/.rvm/gems/ruby-2.7.1:/home/chatwoot/.rvm/gems/ruby-2.7.1@global" \ No newline at end of file diff --git a/deployment/chatwoot-worker.1.service b/deployment/chatwoot-worker.1.service index b3356ba5c..8b3f0b1cb 100644 --- a/deployment/chatwoot-worker.1.service +++ b/deployment/chatwoot-worker.1.service @@ -16,10 +16,10 @@ KillMode=mixed StandardInput=null SyslogIdentifier=%p -Environment="PATH=/home/chatwoot/.rvm/gems/ruby-2.7.0/bin:/home/chatwoot/.rvm/gems/ruby-2.7.0@global/bin:/home/chatwoot/.rvm/rubies/ruby-2.7.0/bin:/home/chatwoot/.rvm/bin:/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin:/snap/bin:/home/chatwoot/.rvm/bin:/home/chatwoot/.rvm/bin" +Environment="PATH=/home/chatwoot/.rvm/gems/ruby-2.7.1/bin:/home/chatwoot/.rvm/gems/ruby-2.7.1@global/bin:/home/chatwoot/.rvm/rubies/ruby-2.7.1/bin:/home/chatwoot/.rvm/bin:/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin:/snap/bin:/home/chatwoot/.rvm/bin:/home/chatwoot/.rvm/bin" Environment="PORT=3000" Environment="RAILS_ENV=production" Environment="NODE_ENV=production" Environment="RAILS_LOG_TO_STDOUT=true" -Environment="GEM_HOME=/home/chatwoot/.rvm/gems/ruby-2.7.0" -Environment="GEM_PATH=/home/chatwoot/.rvm/gems/ruby-2.7.0:/home/chatwoot/.rvm/gems/ruby-2.7.0@global" \ No newline at end of file +Environment="GEM_HOME=/home/chatwoot/.rvm/gems/ruby-2.7.1" +Environment="GEM_PATH=/home/chatwoot/.rvm/gems/ruby-2.7.1:/home/chatwoot/.rvm/gems/ruby-2.7.1@global" \ No newline at end of file diff --git a/deployment/setup.sh b/deployment/setup.sh index 4229873c6..7c19b6b40 100644 --- a/deployment/setup.sh +++ b/deployment/setup.sh @@ -45,8 +45,8 @@ RAILS_ENV=production sudo -i -u chatwoot << EOF rvm --version rvm autolibs disable -rvm install "ruby-2.7.0" -rvm use 2.7.0 --default +rvm install "ruby-2.7.1" +rvm use 2.7.1 --default git clone https://github.com/chatwoot/chatwoot.git cd chatwoot diff --git a/docker/Dockerfile b/docker/Dockerfile index 7b41ca97f..8093fdb02 100644 --- a/docker/Dockerfile +++ b/docker/Dockerfile @@ -1,5 +1,5 @@ # pre-build stage -FROM ruby:2.7.0-alpine AS pre-builder +FROM ruby:2.7.1-alpine AS pre-builder # ARG default to production settings # For development docker-compose file overrides ARGS @@ -36,7 +36,7 @@ COPY Gemfile Gemfile.lock ./ # Do not install development or test gems in production RUN if [ "$RAILS_ENV" = "production" ]; then \ - bundle install -j 4 -r 3 --without development test; \ + bundle config set without 'development test'; bundle install -j 4 -r 3; \ else bundle install -j 4 -r 3; \ fi @@ -51,7 +51,7 @@ RUN if [ "$RAILS_ENV" = "production" ]; then \ fi # final build stage -FROM ruby:2.7.0-alpine +FROM ruby:2.7.1-alpine ARG BUNDLE_WITHOUT="development:test" ENV BUNDLE_WITHOUT ${BUNDLE_WITHOUT} @@ -82,7 +82,7 @@ RUN if [ "$RAILS_ENV" = "production" ]; then \ COPY --from=pre-builder /gems/ /gems/ COPY --from=pre-builder /app /app -# Remove unecessary files +# Remove unnecessary files RUN rm -rf /gems/ruby/2.7.0/cache/*.gem \ && find /gems/ruby/2.7.0/gems/ -name "*.c" -delete \ && find /gems/ruby/2.7.0/gems/ -name "*.o" -delete diff --git a/docs/development/environment-setup/mac-os.md b/docs/development/environment-setup/mac-os.md index 9a1d456ad..d856b5b15 100644 --- a/docs/development/environment-setup/mac-os.md +++ b/docs/development/environment-setup/mac-os.md @@ -37,19 +37,19 @@ source ~/.rvm/scripts/rvm ### Install Ruby -Chatwoot APIs are built on Ruby on Rails, you need install ruby 2.7.0 +Chatwoot APIs are built on Ruby on Rails, you need install ruby 2.7.1 If you are using `rvm` : ```bash -rvm install ruby-2.7.0 -rvm use 2.7.0 +rvm install ruby-2.7.1 +rvm use 2.7.1 ``` If you are using `rbenv` to manage ruby versions do : ```bash -rbenv install 2.7.0 +rbenv install 2.7.1 ``` `rbenv` identifies the ruby version from `.ruby-version` file on the root of the project and loads it automatically. diff --git a/docs/development/environment-setup/ubuntu.md b/docs/development/environment-setup/ubuntu.md index ed062226a..3975f98e7 100644 --- a/docs/development/environment-setup/ubuntu.md +++ b/docs/development/environment-setup/ubuntu.md @@ -33,16 +33,16 @@ Enable `Run command as a login shell` in terminal `Preferences`. Restart your co ### Install Ruby -Chatwoot APIs are built on Ruby on Rails, you need install ruby 2.7.0 +Chatwoot APIs are built on Ruby on Rails, you need install ruby 2.7.1 ```bash -rvm install ruby-2.7.0 +rvm install ruby-2.7.1 ``` -Use ruby 2.7.0 as default +Use ruby 2.7.1 as default ```bash -rvm use 2.7.0 --default +rvm use 2.7.1 --default ``` ### Install Node.js diff --git a/docs/development/environment-setup/windows.md b/docs/development/environment-setup/windows.md index 144f09c36..d2b3266b6 100644 --- a/docs/development/environment-setup/windows.md +++ b/docs/development/environment-setup/windows.md @@ -26,15 +26,15 @@ sudo apt-get update sudo apt-get install git-core curl zlib1g-dev build-essential libssl-dev libreadline-dev libyaml-dev libsqlite3-dev sqlite3 libxml2-dev libxslt1-dev libcurl4-openssl-dev software-properties-common libffi-dev ``` -Install RVM & ruby version 2.7.0 +Install RVM & ruby version 2.7.1 ```bash sudo apt-get install libgdbm-dev libncurses5-dev automake libtool bison libffi-dev gpg --keyserver hkp://keys.gnupg.net --recv-keys 409B6B1796C275462A1703113804BB82D39DC0E3 7D2BAF1CF37B13E2069D6956105BD0E739499BDB curl -sSL https://get.rvm.io | bash -s stable source ~/.rvm/scripts/rvm -rvm install 2.7.0 -rvm use 2.7.0 --default +rvm install 2.7.1 +rvm use 2.7.1 --default ruby -v ``` From f28ec29b8c4c89a85a52ad9c454b342195219ee9 Mon Sep 17 00:00:00 2001 From: Pranav Raj S Date: Sat, 9 May 2020 22:02:43 +0530 Subject: [PATCH 02/42] Feature: Customise widget for bot conversations (#834) * Feature: Customise widget for bot conversations --- .env.example | 3 ++ .../api/v1/widget/conversations_controller.rb | 4 ++ app/javascript/shared/components/ChatForm.vue | 5 ++ app/javascript/widget/App.vue | 2 + app/javascript/widget/api/conversation.js | 14 +++++- .../widget/components/AgentMessage.vue | 13 +++-- app/javascript/widget/helpers/actionCable.js | 6 +++ app/javascript/widget/mixins/configMixin.js | 19 +++++++ .../widget/mixins/specs/configMixin.spec.js | 35 +++++++++++++ app/javascript/widget/store/index.js | 2 + .../widget/store/modules/conversation.js | 4 +- .../store/modules/conversationAttributes.js | 49 +++++++++++++++++++ .../conversationAttributes/actions.spec.js | 32 ++++++++++++ .../conversationAttributes/getters.spec.js | 14 ++++++ .../conversationAttributes/mutations.spec.js | 33 +++++++++++++ app/javascript/widget/store/types.js | 2 + app/javascript/widget/views/Home.vue | 32 ++++++++---- app/listeners/action_cable_listener.rb | 6 ++- app/models/agent_bot.rb | 13 ++--- .../widget/conversations/index.json.jbuilder | 5 ++ app/views/widgets/show.html.erb | 5 ++ ...44639_add_hide_input_flag_to_bot_config.rb | 5 ++ db/schema.rb | 3 +- .../widget/conversations_controller_spec.rb | 18 +++++++ 24 files changed, 298 insertions(+), 26 deletions(-) create mode 100644 app/javascript/widget/mixins/configMixin.js create mode 100644 app/javascript/widget/mixins/specs/configMixin.spec.js create mode 100644 app/javascript/widget/store/modules/conversationAttributes.js create mode 100644 app/javascript/widget/store/modules/specs/conversationAttributes/actions.spec.js create mode 100644 app/javascript/widget/store/modules/specs/conversationAttributes/getters.spec.js create mode 100644 app/javascript/widget/store/modules/specs/conversationAttributes/mutations.spec.js create mode 100644 app/views/api/v1/widget/conversations/index.json.jbuilder create mode 100644 db/migrate/20200509044639_add_hide_input_flag_to_bot_config.rb diff --git a/.env.example b/.env.example index b80e3577d..addf9b685 100644 --- a/.env.example +++ b/.env.example @@ -106,3 +106,6 @@ CHARGEBEE_WEBHOOK_PASSWORD= ## generate a new key value here : https://d3v.one/vapid-key-generator/ # VAPID_PUBLIC_KEY= # VAPID_PRIVATE_KEY= + +## Bot Customizations +USE_INBOX_AVATAR_FOR_BOT=true diff --git a/app/controllers/api/v1/widget/conversations_controller.rb b/app/controllers/api/v1/widget/conversations_controller.rb index 8f8e372fe..cdfbd9d4a 100644 --- a/app/controllers/api/v1/widget/conversations_controller.rb +++ b/app/controllers/api/v1/widget/conversations_controller.rb @@ -3,6 +3,10 @@ class Api::V1::Widget::ConversationsController < Api::V1::Widget::BaseController before_action :set_web_widget before_action :set_contact + def index + @conversation = conversation + end + def toggle_typing head :ok && return if conversation.nil? diff --git a/app/javascript/shared/components/ChatForm.vue b/app/javascript/shared/components/ChatForm.vue index 68af0f053..7922595a2 100644 --- a/app/javascript/shared/components/ChatForm.vue +++ b/app/javascript/shared/components/ChatForm.vue @@ -24,6 +24,7 @@ class="button block" type="submit" :disabled="!isFormValid" + :style="{ background: widgetColor, borderColor: widgetColor }" > {{ $t('COMPONENTS.FORM_BUBBLE.SUBMIT') }} @@ -32,6 +33,7 @@ diff --git a/db/migrate/20200509044639_add_hide_input_flag_to_bot_config.rb b/db/migrate/20200509044639_add_hide_input_flag_to_bot_config.rb new file mode 100644 index 000000000..41b9cda56 --- /dev/null +++ b/db/migrate/20200509044639_add_hide_input_flag_to_bot_config.rb @@ -0,0 +1,5 @@ +class AddHideInputFlagToBotConfig < ActiveRecord::Migration[6.0] + def change + add_column :agent_bots, :hide_input_for_bot_conversations, :boolean, default: false + end +end diff --git a/db/schema.rb b/db/schema.rb index fa3f134e0..39280064a 100644 --- a/db/schema.rb +++ b/db/schema.rb @@ -10,7 +10,7 @@ # # It's strongly recommended that you check this file into your version control system. -ActiveRecord::Schema.define(version: 2020_05_04_144712) do +ActiveRecord::Schema.define(version: 2020_05_09_044639) do # These are extensions that must be enabled in order to support this database enable_extension "pgcrypto" @@ -94,6 +94,7 @@ ActiveRecord::Schema.define(version: 2020_05_04_144712) do t.string "outgoing_url" t.datetime "created_at", precision: 6, null: false t.datetime "updated_at", precision: 6, null: false + t.boolean "hide_input_for_bot_conversations", default: false end create_table "attachments", id: :serial, force: :cascade do |t| diff --git a/spec/controllers/api/v1/widget/conversations_controller_spec.rb b/spec/controllers/api/v1/widget/conversations_controller_spec.rb index 43183a46b..7b3cc6538 100644 --- a/spec/controllers/api/v1/widget/conversations_controller_spec.rb +++ b/spec/controllers/api/v1/widget/conversations_controller_spec.rb @@ -24,4 +24,22 @@ RSpec.describe '/api/v1/widget/conversations/toggle_typing', type: :request do end end end + + describe 'POST /api/v1/widget/conversations' do + context 'with a conversation' do + it 'returns the correct conversation params' do + allow(Rails.configuration.dispatcher).to receive(:dispatch) + get '/api/v1/widget/conversations', + headers: { 'X-Auth-Token' => token }, + params: { website_token: web_widget.website_token }, + as: :json + + expect(response).to have_http_status(:success) + json_response = JSON.parse(response.body) + + expect(json_response['id']).to eq(conversation.display_id) + expect(json_response['status']).to eq(conversation.status) + end + end + end end From 76b98cbed432aa331de60128321ca68aa926f69a Mon Sep 17 00:00:00 2001 From: Pranav Raj S Date: Sun, 10 May 2020 22:20:45 +0530 Subject: [PATCH 03/42] Bug: Fix inbox.agent_bot nil case (#841) --- app/views/widgets/show.html.erb | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/app/views/widgets/show.html.erb b/app/views/widgets/show.html.erb index 571d86919..7602074d7 100644 --- a/app/views/widgets/show.html.erb +++ b/app/views/widgets/show.html.erb @@ -8,7 +8,9 @@ window.chatwootWebChannel = { avatarUrl: '<%= @web_widget.inbox.avatar_url %>', hasAConnectedAgentBot: '<%= @web_widget.inbox.agent_bot&.name %>', - hideInputForBotConversations: <%= ActiveModel::Type::Boolean.new.cast(@web_widget.inbox.agent_bot&.hide_input_for_bot_conversations) %>, + <% if @web_widget.inbox.agent_bot %> + hideInputForBotConversations: <%= @web_widget.inbox.agent_bot.hide_input_for_bot_conversations %>, + <% end %> locale: '<%= @web_widget.account.locale %>', websiteName: '<%= @web_widget.inbox.name %>', websiteToken: '<%= @web_widget.website_token %>', From 905c93b8f800cb51d919f5372f9f6074cbe0e4a1 Mon Sep 17 00:00:00 2001 From: Sony Mathew Date: Sun, 10 May 2020 22:40:36 +0530 Subject: [PATCH 04/42] Feature: Installation global config (#839) (#840) * Renamed concern from Feature to Featurable * Feature: Installation config (#839) * Added new model installtion config with corresponding migrations and specs * Created an installation config yml (key value store model) * Created a config loader module to load the installaltion configs * Added this to the config loader seeder * Changed the account before create hook for default feature enabling to use the feature values from installtion config * Renamed the feature concern to Featurable to follow the naming pattern for concerns * Added comments and specs for modules and places that deemed necessary * Refactored config loader to reduce cognitive complexity (#839) --- .rubocop.yml | 2 + app/models/account.rb | 2 +- .../concerns/{features.rb => featurable.rb} | 7 +- app/models/installation_config.rb | 31 +++++++ config/installation_config.yml | 2 + ...200510112339_create_installation_config.rb | 13 +++ db/schema.rb | 24 ++---- db/seeds.rb | 3 + lib/config_loader.rb | 84 +++++++++++++++++++ spec/lib/config_loader_spec.rb | 46 ++++++++++ spec/models/installation_config_spec.rb | 7 ++ 11 files changed, 203 insertions(+), 18 deletions(-) rename app/models/concerns/{features.rb => featurable.rb} (84%) create mode 100644 app/models/installation_config.rb create mode 100644 config/installation_config.yml create mode 100644 db/migrate/20200510112339_create_installation_config.rb create mode 100644 lib/config_loader.rb create mode 100644 spec/lib/config_loader_spec.rb create mode 100644 spec/models/installation_config_spec.rb diff --git a/.rubocop.yml b/.rubocop.yml index 8d2405e3f..bded7aed3 100644 --- a/.rubocop.yml +++ b/.rubocop.yml @@ -94,6 +94,8 @@ Rails/UniqueValidationWithoutIndex: Exclude: - 'app/models/channel/twitter_profile.rb' - 'app/models/webhook.rb' +RSpec/NamedSubject: + Enabled: false AllCops: Exclude: - 'bin/**/*' diff --git a/app/models/account.rb b/app/models/account.rb index 2b2845ec4..42dcf75ae 100644 --- a/app/models/account.rb +++ b/app/models/account.rb @@ -19,7 +19,7 @@ class Account < ApplicationRecord include Events::Types include Reportable - include Features + include Featurable DEFAULT_QUERY_SETTING = { flag_query_mode: :bit_operator diff --git a/app/models/concerns/features.rb b/app/models/concerns/featurable.rb similarity index 84% rename from app/models/concerns/features.rb rename to app/models/concerns/featurable.rb index d1ae54c5a..720f44cfa 100644 --- a/app/models/concerns/features.rb +++ b/app/models/concerns/featurable.rb @@ -1,4 +1,4 @@ -module Features +module Featurable extend ActiveSupport::Concern QUERY_MODE = { @@ -51,7 +51,10 @@ module Features private def enable_default_features - features_to_enabled = FEATURE_LIST.select { |f| f['enabled'] }.map { |f| f['name'] } + config = InstallationConfig.find_by(name: 'ACCOUNT_LEVEL_FEATURE_DEFAULTS') + return true if config.blank? + + features_to_enabled = config.value.select { |f| f[:enabled] }.map { |f| f[:name] } enable_features(features_to_enabled) end end diff --git a/app/models/installation_config.rb b/app/models/installation_config.rb new file mode 100644 index 000000000..589ab3a36 --- /dev/null +++ b/app/models/installation_config.rb @@ -0,0 +1,31 @@ +# == Schema Information +# +# Table name: installation_configs +# +# id :bigint not null, primary key +# name :string not null +# serialized_value :jsonb not null +# created_at :datetime not null +# updated_at :datetime not null +# +# Indexes +# +# index_installation_configs_on_name_and_created_at (name,created_at) UNIQUE +# +class InstallationConfig < ApplicationRecord + serialize :serialized_value, HashWithIndifferentAccess + + validates :name, presence: true + + default_scope { order(created_at: :desc) } + + def value + serialized_value[:value] + end + + def value=(value_to_assigned) + self.serialized_value = { + value: value_to_assigned + }.with_indifferent_access + end +end diff --git a/config/installation_config.yml b/config/installation_config.yml new file mode 100644 index 000000000..396fe21a8 --- /dev/null +++ b/config/installation_config.yml @@ -0,0 +1,2 @@ +- name: SHOW_WIDGET_HEADER + value: true diff --git a/db/migrate/20200510112339_create_installation_config.rb b/db/migrate/20200510112339_create_installation_config.rb new file mode 100644 index 000000000..7397bb7bf --- /dev/null +++ b/db/migrate/20200510112339_create_installation_config.rb @@ -0,0 +1,13 @@ +class CreateInstallationConfig < ActiveRecord::Migration[6.0] + def change + create_table :installation_configs do |t| + t.string :name, null: false + t.jsonb :serialized_value, null: false, default: '{}' + t.timestamps + end + + add_index :installation_configs, [:name, :created_at], unique: true + + ConfigLoader.new.process + end +end diff --git a/db/schema.rb b/db/schema.rb index 39280064a..7fab7d066 100644 --- a/db/schema.rb +++ b/db/schema.rb @@ -10,7 +10,7 @@ # # It's strongly recommended that you check this file into your version control system. -ActiveRecord::Schema.define(version: 2020_05_09_044639) do +ActiveRecord::Schema.define(version: 2020_05_10_112339) do # These are extensions that must be enabled in order to support this database enable_extension "pgcrypto" @@ -246,6 +246,14 @@ ActiveRecord::Schema.define(version: 2020_05_09_044639) do t.index ["account_id"], name: "index_inboxes_on_account_id" end + create_table "installation_configs", force: :cascade do |t| + t.string "name", null: false + t.jsonb "serialized_value", default: "{}", null: false + t.datetime "created_at", precision: 6, null: false + t.datetime "updated_at", precision: 6, null: false + t.index ["name", "created_at"], name: "index_installation_configs_on_name_and_created_at", unique: true + end + create_table "messages", id: :serial, force: :cascade do |t| t.text "content" t.integer "account_id", null: false @@ -319,20 +327,6 @@ ActiveRecord::Schema.define(version: 2020_05_09_044639) do t.boolean "payment_source_added", default: false end - create_table "super_admins", force: :cascade do |t| - t.string "email", default: "", null: false - t.string "encrypted_password", default: "", null: false - t.datetime "remember_created_at" - t.integer "sign_in_count", default: 0, null: false - t.datetime "current_sign_in_at" - t.datetime "last_sign_in_at" - t.inet "current_sign_in_ip" - t.inet "last_sign_in_ip" - t.datetime "created_at", precision: 6, null: false - t.datetime "updated_at", precision: 6, null: false - t.index ["email"], name: "index_super_admins_on_email", unique: true - end - create_table "taggings", id: :serial, force: :cascade do |t| t.integer "tag_id" t.string "taggable_type" diff --git a/db/seeds.rb b/db/seeds.rb index d6df42782..1a169c966 100644 --- a/db/seeds.rb +++ b/db/seeds.rb @@ -1,3 +1,6 @@ +# loading installation configs +ConfigLoader.new.process + account = Account.create!( name: 'Acme Inc', domain: 'support.chatwoot.com', diff --git a/lib/config_loader.rb b/lib/config_loader.rb new file mode 100644 index 000000000..58abf9c21 --- /dev/null +++ b/lib/config_loader.rb @@ -0,0 +1,84 @@ +class ConfigLoader + DEFAULT_OPTIONS = { + config_path: nil, + reconcile_only_new: true + }.freeze + + def process(options = {}) + options = DEFAULT_OPTIONS.merge(options) + # function of the "reconcile_only_new" flag + # if true, + # it leaves the existing config and feature flags as it is and + # creates the missing configs and feature flags with their default values + # if false, + # then it overwrites existing config and feature flags with default values + # also creates the missing configs and feature flags with their default values + @reconcile_only_new = options[:reconcile_only_new] + + # setting the config path + @config_path = options[:config_path].presence + @config_path ||= Rails.root.join('config') + + # general installation configs + reconcile_general_config + + # default account based feature configs + reconcile_feature_config + end + + private + + def general_configs + @general_configs ||= YAML.safe_load(File.read("#{@config_path}/installation_config.yml")).freeze + end + + def account_features + @account_features ||= YAML.safe_load(File.read("#{@config_path}/features.yml")).freeze + end + + def reconcile_general_config + general_configs.each do |config| + new_config = config.with_indifferent_access + existing_config = InstallationConfig.find_by(name: new_config[:name]) + save_general_config(existing_config, new_config) + end + end + + def save_general_config(existing_config, new_config) + if existing_config + # save config only if reconcile flag is false and existing configs value does not match default value + save_as_new_config(new_config) if !@reconcile_only_new && existing_config.value != new_config[:value] + else + save_as_new_config(new_config) + end + end + + def save_as_new_config(new_config) + config = InstallationConfig.new(name: new_config[:name]) + config.value = new_config[:value] + config.save + end + + def reconcile_feature_config + config = InstallationConfig.find_by(name: 'ACCOUNT_LEVEL_FEATURE_DEFAULTS') + + if config + return false if config.value.to_s == account_features.to_s + + compare_and_save(config) + else + save_as_new_config({ name: 'ACCOUNT_LEVEL_FEATURE_DEFAULTS', value: account_features }) + end + end + + def compare_and_save_feature(config) + features = if @reconcile_only_new + # leave the existing feature flag values as it is and add new feature flags with default values + (account_features + config.value).uniq { |h| h['name'] } + else + # update the existing feature flag values with default values and add new feature flags with default values + (config.value + account_features).uniq { |h| h['name'] } + end + save_as_new_config({ name: 'ACCOUNT_LEVEL_FEATURE_DEFAULTS', value: features }) + end +end diff --git a/spec/lib/config_loader_spec.rb b/spec/lib/config_loader_spec.rb new file mode 100644 index 000000000..60b689872 --- /dev/null +++ b/spec/lib/config_loader_spec.rb @@ -0,0 +1,46 @@ +require 'rails_helper' + +describe ConfigLoader do + subject(:trigger) { described_class.new.process } + + describe 'execute' do + context 'when called with default options' do + it 'creates installation configs' do + expect(InstallationConfig.count).to eq(0) + subject + expect(InstallationConfig.count).to be > 0 + end + + it 'creates account level feature defaults as entry on config table' do + subject + expect(InstallationConfig.find_by(name: 'ACCOUNT_LEVEL_FEATURE_DEFAULTS')).to be_truthy + end + end + + context 'with reconcile_only_new option' do + let(:class_instance) { described_class.new } + let(:config) { { name: 'WHO', value: 'corona' } } + let(:updated_config) { { name: 'WHO', value: 'covid 19' } } + + before do + allow(described_class).to receive(:new).and_return(class_instance) + allow(class_instance).to receive(:general_configs).and_return([config]) + described_class.new.process + end + + it 'being true it should not update existing config value' do + expect(InstallationConfig.find_by(name: 'WHO').value).to eq('corona') + allow(class_instance).to receive(:general_configs).and_return([updated_config]) + described_class.new.process({ reconcile_only_new: true }) + expect(InstallationConfig.find_by(name: 'WHO').value).to eq('corona') + end + + it 'updates the existing config value with new default value' do + expect(InstallationConfig.find_by(name: 'WHO').value).to eq('corona') + allow(class_instance).to receive(:general_configs).and_return([updated_config]) + described_class.new.process({ reconcile_only_new: false }) + expect(InstallationConfig.find_by(name: 'WHO').value).to eq('covid 19') + end + end + end +end diff --git a/spec/models/installation_config_spec.rb b/spec/models/installation_config_spec.rb new file mode 100644 index 000000000..49270f2a8 --- /dev/null +++ b/spec/models/installation_config_spec.rb @@ -0,0 +1,7 @@ +# frozen_string_literal: true + +require 'rails_helper' + +RSpec.describe InstallationConfig do + it { is_expected.to validate_presence_of(:name) } +end From 8859880e55d50cc0124f5edbdbafb32ddf7fd96e Mon Sep 17 00:00:00 2001 From: Sony Mathew Date: Mon, 11 May 2020 19:00:33 +0530 Subject: [PATCH 05/42] Feature: Global Config helper (#844) (#845) * Added a global config helper to easily access installation/global configs * this will fetch the keys from cache with fallback to DB on cache miss * ability to query multiple keys simultaneously * interface to delete the existing global config cache * Added tests for this new helper module --- .rubocop.yml | 1 + lib/global_config.rb | 44 ++++++++++++++++++++++++++++++++++ spec/lib/global_config_spec.rb | 39 ++++++++++++++++++++++++++++++ 3 files changed, 84 insertions(+) create mode 100644 lib/global_config.rb create mode 100644 spec/lib/global_config_spec.rb diff --git a/.rubocop.yml b/.rubocop.yml index bded7aed3..9ea8ca106 100644 --- a/.rubocop.yml +++ b/.rubocop.yml @@ -30,6 +30,7 @@ Style/GlobalVars: Exclude: - 'config/initializers/redis.rb' - 'lib/redis/alfred.rb' + - 'lib/global_config.rb' Metrics/BlockLength: Exclude: - spec/**/* diff --git a/lib/global_config.rb b/lib/global_config.rb new file mode 100644 index 000000000..8c1da224d --- /dev/null +++ b/lib/global_config.rb @@ -0,0 +1,44 @@ +class GlobalConfig + VERSION = 'V1'.freeze + KEY_PREFIX = 'GLOBAL_CONFIG'.freeze + DEFAULT_EXPIRY = 1.day + + class << self + def get(*args) + config_keys = *args + config = {} + + config_keys.each do |config_key| + config[config_key] = load_from_cache(config_key) + end + + config.with_indifferent_access + end + + def clear_cache + cached_keys = $alfred.keys("#{VERSION}:#{KEY_PREFIX}:*") + (cached_keys || []).each do |cached_key| + $alfred.expire(cached_key, 0) + end + end + + private + + def load_from_cache(config_key) + cache_key = "#{VERSION}:#{KEY_PREFIX}:#{config_key}" + cached_value = $alfred.get(cache_key) + + if cached_value.blank? + value_from_db = db_fallback(config_key) + cached_value = { value: value_from_db }.to_json + $alfred.set(cache_key, cached_value, { expiry: DEFAULT_EXPIRY }) + end + + JSON.parse(cached_value)['value'] + end + + def db_fallback(config_key) + InstallationConfig.find_by(name: config_key)&.value + end + end +end diff --git a/spec/lib/global_config_spec.rb b/spec/lib/global_config_spec.rb new file mode 100644 index 000000000..ac9a94368 --- /dev/null +++ b/spec/lib/global_config_spec.rb @@ -0,0 +1,39 @@ +require 'rails_helper' + +describe GlobalConfig do + subject(:trigger) { described_class } + + describe 'execute' do + context 'when called with default options' do + before do + described_class.clear_cache + end + + it 'hit DB for the first call' do + expect(InstallationConfig).to receive(:find_by) + described_class.get('test') + end + + it 'get from cache for subsequent calls' do + # this loads from DB + described_class.get('test') + + # subsequent calls should not hit DB + expect(InstallationConfig).not_to receive(:find_by) + described_class.get('test') + end + + it 'clears cache and fetch from DB next time, when clear_cache is called' do + # this loads from DB and is cached + described_class.get('test') + + # clears the cache + described_class.clear_cache + + # should be loaded from DB + expect(InstallationConfig).to receive(:find_by).with({ name: 'test' }).and_return(nil) + described_class.get('test') + end + end + end +end From c74b5c21d7a822d66bb59d10a2c10246b1bed2eb Mon Sep 17 00:00:00 2001 From: Sojan Jose Date: Mon, 11 May 2020 23:07:22 +0530 Subject: [PATCH 06/42] Feature: Introduce Super Admins (#705) * Feature: Introduce Super Admins - added new devise model for super user - added administrate gem - sample dashboards for users and accounts Co-authored-by: Pranav Raj Sreepuram --- .env.example | 4 - Gemfile | 2 + Gemfile.lock | 32 +++++++ app/assets/config/manifest.js | 2 + app/builders/account_builder.rb | 3 +- .../api/v1/accounts/accounts_controller.rb | 7 +- .../concerns/access_token_auth_helper.rb | 18 ++-- .../super_admin/access_tokens_controller.rb | 44 ++++++++++ .../super_admin/accounts_controller.rb | 44 ++++++++++ .../super_admin/application_controller.rb | 16 ++++ .../super_admin/devise/sessions_controller.rb | 28 ++++++ .../super_admin/super_admins_controller.rb | 44 ++++++++++ .../super_admin/users_controller.rb | 44 ++++++++++ app/dashboards/access_token_dashboard.rb | 66 ++++++++++++++ app/dashboards/account_dashboard.rb | 64 ++++++++++++++ app/dashboards/super_admin_dashboard.rb | 81 +++++++++++++++++ app/dashboards/user_dashboard.rb | 88 +++++++++++++++++++ .../assets/scss/super_admin/index.scss | 5 ++ .../assets/scss/super_admin/pages.scss | 13 +++ app/javascript/packs/superadmin.js | 2 + app/javascript/packs/superadmin_pages.js | 1 + app/models/super_admin.rb | 27 ++++++ .../application/_navigation.html.erb | 27 ++++++ .../super_admin/devise/sessions/new.html.erb | 43 +++++++++ config/initializers/devise.rb | 8 +- config/routes.rb | 26 +++--- ...200410145519_devise_create_super_admins.rb | 45 ++++++++++ db/schema.rb | 14 +++ .../v1/accounts/accounts_controller_spec.rb | 42 +++++++-- .../access_tokens_controller_spec.rb | 24 +++++ .../super_admin/accounts_controller_spec.rb | 26 ++++++ .../super_admins_controller_spec.rb | 24 +++++ .../super_admin/users_controller_spec.rb | 26 ++++++ .../super_admin_controller_spec.rb | 46 ++++++++++ spec/factories/super_admins.rb | 6 ++ spec/models/super_admin_spec.rb | 5 ++ spec/rails_helper.rb | 2 + 37 files changed, 964 insertions(+), 35 deletions(-) create mode 100644 app/controllers/super_admin/access_tokens_controller.rb create mode 100644 app/controllers/super_admin/accounts_controller.rb create mode 100644 app/controllers/super_admin/application_controller.rb create mode 100644 app/controllers/super_admin/devise/sessions_controller.rb create mode 100644 app/controllers/super_admin/super_admins_controller.rb create mode 100644 app/controllers/super_admin/users_controller.rb create mode 100644 app/dashboards/access_token_dashboard.rb create mode 100644 app/dashboards/account_dashboard.rb create mode 100644 app/dashboards/super_admin_dashboard.rb create mode 100644 app/dashboards/user_dashboard.rb create mode 100644 app/javascript/dashboard/assets/scss/super_admin/index.scss create mode 100644 app/javascript/dashboard/assets/scss/super_admin/pages.scss create mode 100644 app/javascript/packs/superadmin.js create mode 100644 app/javascript/packs/superadmin_pages.js create mode 100644 app/models/super_admin.rb create mode 100644 app/views/super_admin/application/_navigation.html.erb create mode 100644 app/views/super_admin/devise/sessions/new.html.erb create mode 100644 db/migrate/20200410145519_devise_create_super_admins.rb create mode 100644 spec/controllers/super_admin/access_tokens_controller_spec.rb create mode 100644 spec/controllers/super_admin/accounts_controller_spec.rb create mode 100644 spec/controllers/super_admin/super_admins_controller_spec.rb create mode 100644 spec/controllers/super_admin/users_controller_spec.rb create mode 100644 spec/controllers/super_admin_controller_spec.rb create mode 100644 spec/factories/super_admins.rb create mode 100644 spec/models/super_admin_spec.rb diff --git a/.env.example b/.env.example index addf9b685..80fec6985 100644 --- a/.env.example +++ b/.env.example @@ -73,10 +73,6 @@ RAILS_LOG_TO_STDOUT=true LOG_LEVEL=info LOG_SIZE=500 -# Credentials to access sidekiq dashboard in production -SIDEKIQ_AUTH_USERNAME= -SIDEKIQ_AUTH_PASSWORD= - ### This environment variables are only required if you are setting up social media channels #facebook FB_VERIFY_TOKEN= diff --git a/Gemfile b/Gemfile index 61b3f4bcd..b9e9c4f4c 100644 --- a/Gemfile +++ b/Gemfile @@ -49,6 +49,8 @@ gem 'devise_token_auth' # authorization gem 'jwt' gem 'pundit' +# super admin +gem 'administrate' ##--- gems for pubsub service ---## # https://karolgalanciak.com/blog/2019/11/30/from-activerecord-callbacks-to-publish-slash-subscribe-pattern-and-event-driven-design/ diff --git a/Gemfile.lock b/Gemfile.lock index 1932d7e72..1659d8cdf 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -84,11 +84,24 @@ GEM activerecord (>= 5.0, < 6.1) addressable (2.7.0) public_suffix (>= 2.0.2, < 5.0) + administrate (0.13.0) + actionpack (>= 4.2) + actionview (>= 4.2) + activerecord (>= 4.2) + autoprefixer-rails (>= 6.0) + datetime_picker_rails (~> 0.0.7) + jquery-rails (>= 4.0) + kaminari (>= 1.0) + momentjs-rails (~> 2.8) + sassc-rails (~> 2.1) + selectize-rails (~> 0.6) annotate (3.1.1) activerecord (>= 3.2, < 7.0) rake (>= 10.4, < 14.0) ast (2.4.0) attr_extras (6.2.3) + autoprefixer-rails (9.7.6) + execjs aws-eventstream (1.1.0) aws-partitions (1.310.0) aws-sdk-core (3.94.1) @@ -141,6 +154,8 @@ GEM concurrent-ruby (1.1.6) connection_pool (2.2.2) crass (1.0.6) + datetime_picker_rails (0.0.7) + momentjs-rails (>= 2.8.1) declarative (0.0.10) declarative-option (0.1.0) descendants_tracker (0.0.4) @@ -235,6 +250,10 @@ GEM jbuilder (2.10.0) activesupport (>= 5.0.0) jmespath (1.4.0) + jquery-rails (4.3.5) + rails-dom-testing (>= 1, < 3) + railties (>= 4.2.0) + thor (>= 0.14, < 2.0) json (2.3.0) json_pure (2.3.0) jwt (2.2.1) @@ -278,6 +297,8 @@ GEM mini_mime (1.0.2) mini_portile2 (2.4.0) minitest (5.14.0) + momentjs-rails (2.20.1) + railties (>= 3.1) msgpack (1.3.3) multi_json (1.14.1) multi_xml (0.6.0) @@ -406,6 +427,14 @@ GEM sass-listen (4.0.0) rb-fsevent (~> 0.9, >= 0.9.4) rb-inotify (~> 0.9, >= 0.9.7) + sassc (2.3.0) + ffi (~> 1.9) + sassc-rails (2.1.2) + railties (>= 4.0.0) + sassc (>= 2.0) + sprockets (> 3.0) + sprockets-rails + tilt scout_apm (2.6.7) parser scss_lint (0.59.0) @@ -413,6 +442,7 @@ GEM seed_dump (3.3.1) activerecord (>= 4) activesupport (>= 4) + selectize-rails (0.12.6) semantic_range (2.3.0) sentry-raven (3.0.0) faraday (>= 1.0) @@ -451,6 +481,7 @@ GEM telephone_number (1.4.6) thor (0.20.3) thread_safe (0.3.6) + tilt (2.0.10) time_diff (0.3.0) activesupport i18n @@ -505,6 +536,7 @@ PLATFORMS DEPENDENCIES action-cable-testing acts-as-taggable-on + administrate annotate attr_extras aws-sdk-s3 diff --git a/app/assets/config/manifest.js b/app/assets/config/manifest.js index ac907b367..9b826819b 100644 --- a/app/assets/config/manifest.js +++ b/app/assets/config/manifest.js @@ -1 +1,3 @@ //= link_tree ../images +//= link administrate/application.css +//= link administrate/application.js diff --git a/app/builders/account_builder.rb b/app/builders/account_builder.rb index 126eedce0..9c724bd43 100644 --- a/app/builders/account_builder.rb +++ b/app/builders/account_builder.rb @@ -2,7 +2,7 @@ class AccountBuilder include CustomExceptions::Account - pattr_initialize [:account_name!, :email!] + pattr_initialize [:account_name!, :email!, :confirmed!] def perform validate_email @@ -46,6 +46,7 @@ class AccountBuilder password: password, password_confirmation: password, name: email_to_name(@email)) + @user.confirm if @confirmed if @user.save! link_user_to_account(@user, @account) @user diff --git a/app/controllers/api/v1/accounts/accounts_controller.rb b/app/controllers/api/v1/accounts/accounts_controller.rb index 0fd5dc7cf..29e26929b 100644 --- a/app/controllers/api/v1/accounts/accounts_controller.rb +++ b/app/controllers/api/v1/accounts/accounts_controller.rb @@ -16,7 +16,8 @@ class Api::V1::Accounts::AccountsController < Api::BaseController def create @user = AccountBuilder.new( account_name: account_params[:account_name], - email: account_params[:email] + email: account_params[:email], + confirmed: confirmed? ).perform if @user send_auth_headers(@user) @@ -40,6 +41,10 @@ class Api::V1::Accounts::AccountsController < Api::BaseController authorize(Account) end + def confirmed? + super_admin? && params[:confirmed] + end + def fetch_account @account = current_user.accounts.find(params[:id]) end diff --git a/app/controllers/concerns/access_token_auth_helper.rb b/app/controllers/concerns/access_token_auth_helper.rb index e7af9e116..3d6f55674 100644 --- a/app/controllers/concerns/access_token_auth_helper.rb +++ b/app/controllers/concerns/access_token_auth_helper.rb @@ -4,17 +4,25 @@ module AccessTokenAuthHelper 'api/v1/accounts/conversations/messages' => ['create'] }.freeze - def authenticate_access_token! + def ensure_access_token token = request.headers[:api_access_token] || request.headers[:HTTP_API_ACCESS_TOKEN] - access_token = AccessToken.find_by(token: token) - render_unauthorized('Invalid Access Token') && return unless access_token + @access_token = AccessToken.find_by(token: token) if token.present? + end - token_owner = access_token.owner - @resource = token_owner + def authenticate_access_token! + ensure_access_token + render_unauthorized('Invalid Access Token') && return if @access_token.blank? + + @resource = @access_token.owner + end + + def super_admin? + @resource.present? && @resource.is_a?(SuperAdmin) end def validate_bot_access_token! return if current_user.is_a?(User) + return if super_admin? return if agent_bot_accessible? render_unauthorized('Access to this endpoint is not authorized for bots') diff --git a/app/controllers/super_admin/access_tokens_controller.rb b/app/controllers/super_admin/access_tokens_controller.rb new file mode 100644 index 000000000..a8a2669c4 --- /dev/null +++ b/app/controllers/super_admin/access_tokens_controller.rb @@ -0,0 +1,44 @@ +class SuperAdmin::AccessTokensController < SuperAdmin::ApplicationController + # Overwrite any of the RESTful controller actions to implement custom behavior + # For example, you may want to send an email after a foo is updated. + # + # def update + # super + # send_foo_updated_email(requested_resource) + # end + + # Override this method to specify custom lookup behavior. + # This will be used to set the resource for the `show`, `edit`, and `update` + # actions. + # + # def find_resource(param) + # Foo.find_by!(slug: param) + # end + + # The result of this lookup will be available as `requested_resource` + + # Override this if you have certain roles that require a subset + # this will be used to set the records shown on the `index` action. + # + # def scoped_resource + # if current_user.super_admin? + # resource_class + # else + # resource_class.with_less_stuff + # end + # end + + # Override `resource_params` if you want to transform the submitted + # data before it's persisted. For example, the following would turn all + # empty values into nil values. It uses other APIs such as `resource_class` + # and `dashboard`: + # + # def resource_params + # params.require(resource_class.model_name.param_key). + # permit(dashboard.permitted_attributes). + # transform_values { |value| value == "" ? nil : value } + # end + + # See https://administrate-prototype.herokuapp.com/customizing_controller_actions + # for more information +end diff --git a/app/controllers/super_admin/accounts_controller.rb b/app/controllers/super_admin/accounts_controller.rb new file mode 100644 index 000000000..4d35fa1d8 --- /dev/null +++ b/app/controllers/super_admin/accounts_controller.rb @@ -0,0 +1,44 @@ +class SuperAdmin::AccountsController < SuperAdmin::ApplicationController + # Overwrite any of the RESTful controller actions to implement custom behavior + # For example, you may want to send an email after a foo is updated. + # + # def update + # super + # send_foo_updated_email(requested_resource) + # end + + # Override this method to specify custom lookup behavior. + # This will be used to set the resource for the `show`, `edit`, and `update` + # actions. + # + # def find_resource(param) + # Foo.find_by!(slug: param) + # end + + # The result of this lookup will be available as `requested_resource` + + # Override this if you have certain roles that require a subset + # this will be used to set the records shown on the `index` action. + # + # def scoped_resource + # if current_user.super_admin? + # resource_class + # else + # resource_class.with_less_stuff + # end + # end + + # Override `resource_params` if you want to transform the submitted + # data before it's persisted. For example, the following would turn all + # empty values into nil values. It uses other APIs such as `resource_class` + # and `dashboard`: + # + # def resource_params + # params.require(resource_class.model_name.param_key). + # permit(dashboard.permitted_attributes). + # transform_values { |value| value == "" ? nil : value } + # end + + # See https://administrate-prototype.herokuapp.com/customizing_controller_actions + # for more information +end diff --git a/app/controllers/super_admin/application_controller.rb b/app/controllers/super_admin/application_controller.rb new file mode 100644 index 000000000..463ad30e6 --- /dev/null +++ b/app/controllers/super_admin/application_controller.rb @@ -0,0 +1,16 @@ +# All Administrate controllers inherit from this +# `Administrate::ApplicationController`, making it the ideal place to put +# authentication logic or other before_actions. +# +# If you want to add pagination or other controller-level concerns, +# you're free to overwrite the RESTful controller actions. +class SuperAdmin::ApplicationController < Administrate::ApplicationController + # authenticiation done via devise : SuperAdmin Model + before_action :authenticate_super_admin! + + # Override this value to specify the number of elements to display at a time + # on index pages. Defaults to 20. + # def records_per_page + # params[:per_page] || 20 + # end +end diff --git a/app/controllers/super_admin/devise/sessions_controller.rb b/app/controllers/super_admin/devise/sessions_controller.rb new file mode 100644 index 000000000..fe046a522 --- /dev/null +++ b/app/controllers/super_admin/devise/sessions_controller.rb @@ -0,0 +1,28 @@ +# frozen_string_literal: true + +class SuperAdmin::Devise::SessionsController < Devise::SessionsController + def new + self.resource = resource_class.new(sign_in_params) + end + + def create + return unless valid_credentials? + + sign_in(@super_admin, scope: :super_admin) + flash.discard + redirect_to super_admin_users_path + end + + def destroy + sign_out + flash.discard + redirect_to '/' + end + + private + + def valid_credentials? + @super_admin = SuperAdmin.find_by!(email: params[:super_admin][:email]) + @super_admin.valid_password?(params[:super_admin][:password]) + end +end diff --git a/app/controllers/super_admin/super_admins_controller.rb b/app/controllers/super_admin/super_admins_controller.rb new file mode 100644 index 000000000..16d91a151 --- /dev/null +++ b/app/controllers/super_admin/super_admins_controller.rb @@ -0,0 +1,44 @@ +class SuperAdmin::SuperAdminsController < SuperAdmin::ApplicationController + # Overwrite any of the RESTful controller actions to implement custom behavior + # For example, you may want to send an email after a foo is updated. + # + # def update + # super + # send_foo_updated_email(requested_resource) + # end + + # Override this method to specify custom lookup behavior. + # This will be used to set the resource for the `show`, `edit`, and `update` + # actions. + # + # def find_resource(param) + # Foo.find_by!(slug: param) + # end + + # The result of this lookup will be available as `requested_resource` + + # Override this if you have certain roles that require a subset + # this will be used to set the records shown on the `index` action. + # + # def scoped_resource + # if current_user.super_admin? + # resource_class + # else + # resource_class.with_less_stuff + # end + # end + + # Override `resource_params` if you want to transform the submitted + # data before it's persisted. For example, the following would turn all + # empty values into nil values. It uses other APIs such as `resource_class` + # and `dashboard`: + # + # def resource_params + # params.require(resource_class.model_name.param_key). + # permit(dashboard.permitted_attributes). + # transform_values { |value| value == "" ? nil : value } + # end + + # See https://administrate-prototype.herokuapp.com/customizing_controller_actions + # for more information +end diff --git a/app/controllers/super_admin/users_controller.rb b/app/controllers/super_admin/users_controller.rb new file mode 100644 index 000000000..613670849 --- /dev/null +++ b/app/controllers/super_admin/users_controller.rb @@ -0,0 +1,44 @@ +class SuperAdmin::UsersController < SuperAdmin::ApplicationController + # Overwrite any of the RESTful controller actions to implement custom behavior + # For example, you may want to send an email after a foo is updated. + # + # def update + # super + # send_foo_updated_email(requested_resource) + # end + + # Override this method to specify custom lookup behavior. + # This will be used to set the resource for the `show`, `edit`, and `update` + # actions. + # + # def find_resource(param) + # Foo.find_by!(slug: param) + # end + + # The result of this lookup will be available as `requested_resource` + + # Override this if you have certain roles that require a subset + # this will be used to set the records shown on the `index` action. + # + # def scoped_resource + # if current_user.super_admin? + # resource_class + # else + # resource_class.with_less_stuff + # end + # end + + # Override `resource_params` if you want to transform the submitted + # data before it's persisted. For example, the following would turn all + # empty values into nil values. It uses other APIs such as `resource_class` + # and `dashboard`: + # + # def resource_params + # params.require(resource_class.model_name.param_key). + # permit(dashboard.permitted_attributes). + # transform_values { |value| value == "" ? nil : value } + # end + + # See https://administrate-prototype.herokuapp.com/customizing_controller_actions + # for more information +end diff --git a/app/dashboards/access_token_dashboard.rb b/app/dashboards/access_token_dashboard.rb new file mode 100644 index 000000000..bdc50a7db --- /dev/null +++ b/app/dashboards/access_token_dashboard.rb @@ -0,0 +1,66 @@ +require 'administrate/base_dashboard' + +class AccessTokenDashboard < Administrate::BaseDashboard + # ATTRIBUTE_TYPES + # a hash that describes the type of each of the model's fields. + # + # Each different type represents an Administrate::Field object, + # which determines how the attribute is displayed + # on pages throughout the dashboard. + ATTRIBUTE_TYPES = { + owner: Field::Polymorphic, + id: Field::Number, + token: Field::String, + created_at: Field::DateTime, + updated_at: Field::DateTime + }.freeze + + # COLLECTION_ATTRIBUTES + # an array of attributes that will be displayed on the model's index page. + # + # By default, it's limited to four items to reduce clutter on index pages. + # Feel free to add, remove, or rearrange items. + COLLECTION_ATTRIBUTES = %i[ + owner + id + token + created_at + ].freeze + + # SHOW_PAGE_ATTRIBUTES + # an array of attributes that will be displayed on the model's show page. + SHOW_PAGE_ATTRIBUTES = %i[ + owner + id + token + created_at + updated_at + ].freeze + + # FORM_ATTRIBUTES + # an array of attributes that will be displayed + # on the model's form (`new` and `edit`) pages. + FORM_ATTRIBUTES = %i[ + owner + token + ].freeze + + # COLLECTION_FILTERS + # a hash that defines filters that can be used while searching via the search + # field of the dashboard. + # + # For example to add an option to search for open resources by typing "open:" + # in the search field: + # + # COLLECTION_FILTERS = { + # open: ->(resources) { resources.where(open: true) } + # }.freeze + COLLECTION_FILTERS = {}.freeze + + # Overwrite this method to customize how access tokens are displayed + # across all pages of the admin dashboard. + # + # def display_resource(access_token) + # "AccessToken ##{access_token.id}" + # end +end diff --git a/app/dashboards/account_dashboard.rb b/app/dashboards/account_dashboard.rb new file mode 100644 index 000000000..d80abc199 --- /dev/null +++ b/app/dashboards/account_dashboard.rb @@ -0,0 +1,64 @@ +require 'administrate/base_dashboard' + +class AccountDashboard < Administrate::BaseDashboard + # ATTRIBUTE_TYPES + # a hash that describes the type of each of the model's fields. + # + # Each different type represents an Administrate::Field object, + # which determines how the attribute is displayed + # on pages throughout the dashboard. + ATTRIBUTE_TYPES = { + id: Field::Number, + name: Field::String, + created_at: Field::DateTime, + updated_at: Field::DateTime, + locale: Field::String.with_options(searchable: false) + }.freeze + + # COLLECTION_ATTRIBUTES + # an array of attributes that will be displayed on the model's index page. + # + # By default, it's limited to four items to reduce clutter on index pages. + # Feel free to add, remove, or rearrange items. + COLLECTION_ATTRIBUTES = %i[ + name + locale + ].freeze + + # SHOW_PAGE_ATTRIBUTES + # an array of attributes that will be displayed on the model's show page. + SHOW_PAGE_ATTRIBUTES = %i[ + id + name + created_at + updated_at + locale + ].freeze + + # FORM_ATTRIBUTES + # an array of attributes that will be displayed + # on the model's form (`new` and `edit`) pages. + FORM_ATTRIBUTES = %i[ + name + locale + ].freeze + + # COLLECTION_FILTERS + # a hash that defines filters that can be used while searching via the search + # field of the dashboard. + # + # For example to add an option to search for open resources by typing "open:" + # in the search field: + # + # COLLECTION_FILTERS = { + # open: ->(resources) { resources.where(open: true) } + # }.freeze + COLLECTION_FILTERS = {}.freeze + + # Overwrite this method to customize how accounts are displayed + # across all pages of the admin dashboard. + # + # def display_resource(account) + # "Account ##{account.id}" + # end +end diff --git a/app/dashboards/super_admin_dashboard.rb b/app/dashboards/super_admin_dashboard.rb new file mode 100644 index 000000000..4ceab3a17 --- /dev/null +++ b/app/dashboards/super_admin_dashboard.rb @@ -0,0 +1,81 @@ +require 'administrate/base_dashboard' + +class SuperAdminDashboard < Administrate::BaseDashboard + # ATTRIBUTE_TYPES + # a hash that describes the type of each of the model's fields. + # + # Each different type represents an Administrate::Field object, + # which determines how the attribute is displayed + # on pages throughout the dashboard. + ATTRIBUTE_TYPES = { + id: Field::Number, + email: Field::String, + access_token: Field::HasOne, + remember_created_at: Field::DateTime, + sign_in_count: Field::Number, + current_sign_in_at: Field::DateTime, + last_sign_in_at: Field::DateTime, + current_sign_in_ip: Field::String.with_options(searchable: false), + last_sign_in_ip: Field::String.with_options(searchable: false), + created_at: Field::DateTime, + updated_at: Field::DateTime + }.freeze + + # COLLECTION_ATTRIBUTES + # an array of attributes that will be displayed on the model's index page. + # + # By default, it's limited to four items to reduce clutter on index pages. + # Feel free to add, remove, or rearrange items. + COLLECTION_ATTRIBUTES = %i[ + id + email + access_token + ].freeze + + # SHOW_PAGE_ATTRIBUTES + # an array of attributes that will be displayed on the model's show page. + SHOW_PAGE_ATTRIBUTES = %i[ + id + email + remember_created_at + sign_in_count + current_sign_in_at + last_sign_in_at + current_sign_in_ip + last_sign_in_ip + created_at + updated_at + ].freeze + + # FORM_ATTRIBUTES + # an array of attributes that will be displayed + # on the model's form (`new` and `edit`) pages. + FORM_ATTRIBUTES = %i[ + email + remember_created_at + sign_in_count + current_sign_in_at + last_sign_in_at + current_sign_in_ip + last_sign_in_ip + ].freeze + + # COLLECTION_FILTERS + # a hash that defines filters that can be used while searching via the search + # field of the dashboard. + # + # For example to add an option to search for open resources by typing "open:" + # in the search field: + # + # COLLECTION_FILTERS = { + # open: ->(resources) { resources.where(open: true) } + # }.freeze + COLLECTION_FILTERS = {}.freeze + + # Overwrite this method to customize how super admins are displayed + # across all pages of the admin dashboard. + # + # def display_resource(super_admin) + # "SuperAdmin ##{super_admin.id}" + # end +end diff --git a/app/dashboards/user_dashboard.rb b/app/dashboards/user_dashboard.rb new file mode 100644 index 000000000..e8d24eae2 --- /dev/null +++ b/app/dashboards/user_dashboard.rb @@ -0,0 +1,88 @@ +require 'administrate/base_dashboard' + +class UserDashboard < Administrate::BaseDashboard + # ATTRIBUTE_TYPES + # a hash that describes the type of each of the model's fields. + # + # Each different type represents an Administrate::Field object, + # which determines how the attribute is displayed + # on pages throughout the dashboard. + ATTRIBUTE_TYPES = { + account_users: Field::HasMany, + accounts: Field::HasMany, + invitees: Field::HasMany.with_options(class_name: 'User'), + id: Field::Number, + provider: Field::String, + uid: Field::String, + reset_password_token: Field::String, + reset_password_sent_at: Field::DateTime, + remember_created_at: Field::DateTime, + sign_in_count: Field::Number, + current_sign_in_at: Field::DateTime, + last_sign_in_at: Field::DateTime, + current_sign_in_ip: Field::String, + last_sign_in_ip: Field::String, + confirmation_token: Field::String, + confirmed_at: Field::DateTime, + confirmation_sent_at: Field::DateTime, + unconfirmed_email: Field::String, + name: Field::String, + nickname: Field::String, + email: Field::String, + tokens: Field::String.with_options(searchable: false), + created_at: Field::DateTime, + updated_at: Field::DateTime, + pubsub_token: Field::String + }.freeze + + # COLLECTION_ATTRIBUTES + # an array of attributes that will be displayed on the model's index page. + # + # By default, it's limited to four items to reduce clutter on index pages. + # Feel free to add, remove, or rearrange items. + COLLECTION_ATTRIBUTES = %i[ + name + email + ].freeze + + # SHOW_PAGE_ATTRIBUTES + # an array of attributes that will be displayed on the model's show page. + SHOW_PAGE_ATTRIBUTES = %i[ + accounts + id + unconfirmed_email + name + nickname + email + created_at + updated_at + ].freeze + + # FORM_ATTRIBUTES + # an array of attributes that will be displayed + # on the model's form (`new` and `edit`) pages. + FORM_ATTRIBUTES = %i[ + name + nickname + email + ].freeze + + # COLLECTION_FILTERS + # a hash that defines filters that can be used while searching via the search + # field of the dashboard. + # + # For example to add an option to search for open resources by typing "open:" + # in the search field: + # + # COLLECTION_FILTERS = { + # open: ->(resources) { resources.where(open: true) } + # }.freeze + COLLECTION_FILTERS = {}.freeze + + # Overwrite this method to customize how users are displayed + # across all pages of the admin dashboard. + # + # def display_resource(user) + # "User ##{user.id}" + # end +end diff --git a/app/javascript/dashboard/assets/scss/super_admin/index.scss b/app/javascript/dashboard/assets/scss/super_admin/index.scss new file mode 100644 index 000000000..c45cf2e22 --- /dev/null +++ b/app/javascript/dashboard/assets/scss/super_admin/index.scss @@ -0,0 +1,5 @@ +@import '../variables'; + +.superadmin-body { + background: $color-background; +} diff --git a/app/javascript/dashboard/assets/scss/super_admin/pages.scss b/app/javascript/dashboard/assets/scss/super_admin/pages.scss new file mode 100644 index 000000000..91b62d671 --- /dev/null +++ b/app/javascript/dashboard/assets/scss/super_admin/pages.scss @@ -0,0 +1,13 @@ +@import 'shared/assets/fonts/inter'; +@import '../variables'; + +body { + background-color: $color-background; + font-family: Inter; +} + +.button { + background-color: $color-woot; + border-radius: 1px solid $color-woot; + color: $color-white; +} diff --git a/app/javascript/packs/superadmin.js b/app/javascript/packs/superadmin.js new file mode 100644 index 000000000..90e58bd5e --- /dev/null +++ b/app/javascript/packs/superadmin.js @@ -0,0 +1,2 @@ +import '../dashboard/assets/scss/app.scss'; +import '../dashboard/assets/scss/super_admin/index.scss'; diff --git a/app/javascript/packs/superadmin_pages.js b/app/javascript/packs/superadmin_pages.js new file mode 100644 index 000000000..4870b6f0f --- /dev/null +++ b/app/javascript/packs/superadmin_pages.js @@ -0,0 +1 @@ +import '../dashboard/assets/scss/super_admin/pages.scss'; diff --git a/app/models/super_admin.rb b/app/models/super_admin.rb new file mode 100644 index 000000000..72b1ceb8a --- /dev/null +++ b/app/models/super_admin.rb @@ -0,0 +1,27 @@ +# == Schema Information +# +# Table name: super_admins +# +# id :bigint not null, primary key +# current_sign_in_at :datetime +# current_sign_in_ip :inet +# email :string default(""), not null +# encrypted_password :string default(""), not null +# last_sign_in_at :datetime +# last_sign_in_ip :inet +# remember_created_at :datetime +# sign_in_count :integer default(0), not null +# created_at :datetime not null +# updated_at :datetime not null +# +# Indexes +# +# index_super_admins_on_email (email) UNIQUE +# +class SuperAdmin < ApplicationRecord + # Include default devise modules. Others available are: + # :confirmable, :lockable, :timeoutable, :trackable and :omniauthable + devise :database_authenticatable, :trackable, :rememberable, :validatable + + include AccessTokenable +end diff --git a/app/views/super_admin/application/_navigation.html.erb b/app/views/super_admin/application/_navigation.html.erb new file mode 100644 index 000000000..348309e7b --- /dev/null +++ b/app/views/super_admin/application/_navigation.html.erb @@ -0,0 +1,27 @@ +<%# +# Navigation + +This partial is used to display the navigation in Administrate. +By default, the navigation contains navigation links +for all resources in the admin dashboard, +as defined by the routes in the `admin/` namespace +%> + +<%= javascript_pack_tag 'superadmin_pages' %> +<%= stylesheet_pack_tag 'superadmin_pages' %> + + + diff --git a/app/views/super_admin/devise/sessions/new.html.erb b/app/views/super_admin/devise/sessions/new.html.erb new file mode 100644 index 000000000..294ee66ee --- /dev/null +++ b/app/views/super_admin/devise/sessions/new.html.erb @@ -0,0 +1,43 @@ + + + + SuperAdmin | Chatwoot + <%= javascript_pack_tag 'superadmin' %> + <%= stylesheet_pack_tag 'superadmin' %> + + +
+ + + diff --git a/config/initializers/devise.rb b/config/initializers/devise.rb index ce0010637..80eb40433 100644 --- a/config/initializers/devise.rb +++ b/config/initializers/devise.rb @@ -6,7 +6,7 @@ Devise.setup do |config| # confirmation, reset password and unlock tokens in the database. # Devise will use the `secret_key_base` as its `secret_key` # by default. You can change it below and use your own secret key. - # config.secret_key = 'dff4665a082305d28b485d1d763d0d3e52e2577220eaa551836862a3dbca1aade309fe7ceed35180ac494cbc27bd2f5f84d45e4d19530598d1bd899dcbb115e1' + # config.secret_key = 'dff4665a082305d28b485d1d763d0d3e52e2577220eaa551836862a3dbca1aade309fe7ceed35180ac494cbc27bd2f5f84d45e1' # ==> Mailer Configuration # Configure the e-mail address which will be shown in Devise::Mailer, @@ -220,15 +220,15 @@ Devise.setup do |config| # Turn scoped views on. Before rendering "sessions/new", it will first check for # "users/sessions/new". It's turned off by default because it's slower if you # are using only default views. - # config.scoped_views = false + config.scoped_views = true # Configure the default scope given to Warden. By default it's the first # devise role declared in your routes (usually :user). - # config.default_scope = :user + config.default_scope = :user # Set this configuration to false if you want /users/sign_out to sign out # only the current scope. By default, Devise signs out all scopes. - # config.sign_out_all_scopes = true + config.sign_out_all_scopes = true # ==> Navigation configuration # Lists the formats that should be treated as navigational. Formats like diff --git a/config/routes.rb b/config/routes.rb index 50331d25a..35f162e0d 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -168,20 +168,20 @@ Rails.application.routes.draw do # Internal Monitoring Routes require 'sidekiq/web' - scope :monitoring do - # Sidekiq should use basic auth in production environment - if Rails.env.production? - Sidekiq::Web.use Rack::Auth::Basic do |username, password| - ENV['SIDEKIQ_AUTH_USERNAME'] && - ENV['SIDEKIQ_AUTH_PASSWORD'] && - ActiveSupport::SecurityUtils.secure_compare(::Digest::SHA256.hexdigest(username), - ::Digest::SHA256.hexdigest(ENV['SIDEKIQ_AUTH_USERNAME'])) && - ActiveSupport::SecurityUtils.secure_compare(::Digest::SHA256.hexdigest(password), - ::Digest::SHA256.hexdigest(ENV['SIDEKIQ_AUTH_PASSWORD'])) - end - end + devise_for :super_admins, path: 'super_admin', controllers: { sessions: 'super_admin/devise/sessions' } + devise_scope :super_admin do + get 'super_admin/logout', to: 'super_admin/devise/sessions#destroy' + namespace :super_admin do + resources :users + resources :accounts + resources :super_admins + resources :access_tokens - mount Sidekiq::Web, at: '/sidekiq' + root to: 'users#index' + end + authenticated :super_admin do + mount Sidekiq::Web => '/monitoring/sidekiq' + end end # --------------------------------------------------------------------- diff --git a/db/migrate/20200410145519_devise_create_super_admins.rb b/db/migrate/20200410145519_devise_create_super_admins.rb new file mode 100644 index 000000000..4a74847dc --- /dev/null +++ b/db/migrate/20200410145519_devise_create_super_admins.rb @@ -0,0 +1,45 @@ +# frozen_string_literal: true + +class DeviseCreateSuperAdmins < ActiveRecord::Migration[6.0] + def change + return if ActiveRecord::Base.connection.table_exists? 'super_admins' + + create_table :super_admins do |t| + ## Database authenticatable + t.string :email, null: false, default: '' + t.string :encrypted_password, null: false, default: '' + + ## Recoverable + # t.string :reset_password_token + # t.datetime :reset_password_sent_at + + ## Rememberable + t.datetime :remember_created_at + + ## Trackable + t.integer :sign_in_count, default: 0, null: false + t.datetime :current_sign_in_at + t.datetime :last_sign_in_at + t.inet :current_sign_in_ip + t.inet :last_sign_in_ip + + ## Confirmable + # t.string :confirmation_token + # t.datetime :confirmed_at + # t.datetime :confirmation_sent_at + # t.string :unconfirmed_email # Only if using reconfirmable + + ## Lockable + # t.integer :failed_attempts, default: 0, null: false # Only if lock strategy is :failed_attempts + # t.string :unlock_token # Only if unlock strategy is :email or :both + # t.datetime :locked_at + + t.timestamps null: false + end + + add_index :super_admins, :email, unique: true + # add_index :super_admins, :reset_password_token, unique: true + # add_index :super_admins, :confirmation_token, unique: true + # add_index :super_admins, :unlock_token, unique: true + end +end diff --git a/db/schema.rb b/db/schema.rb index 7fab7d066..6965c2d03 100644 --- a/db/schema.rb +++ b/db/schema.rb @@ -327,6 +327,20 @@ ActiveRecord::Schema.define(version: 2020_05_10_112339) do t.boolean "payment_source_added", default: false end + create_table "super_admins", force: :cascade do |t| + t.string "email", default: "", null: false + t.string "encrypted_password", default: "", null: false + t.datetime "remember_created_at" + t.integer "sign_in_count", default: 0, null: false + t.datetime "current_sign_in_at" + t.datetime "last_sign_in_at" + t.inet "current_sign_in_ip" + t.inet "last_sign_in_ip" + t.datetime "created_at", precision: 6, null: false + t.datetime "updated_at", precision: 6, null: false + t.index ["email"], name: "index_super_admins_on_email", unique: true + end + create_table "taggings", id: :serial, force: :cascade do |t| t.integer "tag_id" t.string "taggable_type" diff --git a/spec/controllers/api/v1/accounts/accounts_controller_spec.rb b/spec/controllers/api/v1/accounts/accounts_controller_spec.rb index 70b6953a3..955cf1e72 100644 --- a/spec/controllers/api/v1/accounts/accounts_controller_spec.rb +++ b/spec/controllers/api/v1/accounts/accounts_controller_spec.rb @@ -2,9 +2,10 @@ require 'rails_helper' RSpec.describe 'Accounts API', type: :request do describe 'POST /api/v1/accounts' do + let(:email) { Faker::Internet.email } + context 'when posting to accounts with correct parameters' do let(:account_builder) { double } - let(:email) { Faker::Internet.email } let(:account) { create(:account) } let(:user) { create(:user, email: email, account: account) } @@ -22,7 +23,7 @@ RSpec.describe 'Accounts API', type: :request do params: params, as: :json - expect(AccountBuilder).to have_received(:new).with(params) + expect(AccountBuilder).to have_received(:new).with(params.merge(confirmed: false)) expect(account_builder).to have_received(:perform) expect(response.headers.keys).to include('access-token', 'token-type', 'client', 'expiry', 'uid') end @@ -36,16 +37,45 @@ RSpec.describe 'Accounts API', type: :request do params: params, as: :json - expect(AccountBuilder).to have_received(:new).with(params) + expect(AccountBuilder).to have_received(:new).with(params.merge(confirmed: false)) + expect(account_builder).to have_received(:perform) + expect(response).to have_http_status(:forbidden) + expect(response.body).to eq({ message: I18n.t('errors.signup.failed') }.to_json) + end + + it 'ignores confirmed param when called with out super admin token' do + allow(account_builder).to receive(:perform).and_return(nil) + + params = { account_name: 'test', email: email, confirmed: true } + + post api_v1_accounts_url, + params: params, + as: :json + + expect(AccountBuilder).to have_received(:new).with(params.merge(confirmed: false)) expect(account_builder).to have_received(:perform) expect(response).to have_http_status(:forbidden) expect(response.body).to eq({ message: I18n.t('errors.signup.failed') }.to_json) end end - context 'when ENABLE_ACCOUNT_SIGNUP env variable is set to false' do - let(:email) { Faker::Internet.email } + context 'when called with super admin token' do + let(:super_admin) { create(:super_admin) } + it 'calls account builder with confirmed true when confirmed param is passed' do + params = { account_name: 'test', email: email, confirmed: true } + + post api_v1_accounts_url, + params: params, + headers: { api_access_token: super_admin.access_token.token }, + as: :json + + expect(User.find_by(email: email).confirmed?).to eq(true) + expect(response.headers.keys).to include('access-token', 'token-type', 'client', 'expiry', 'uid') + end + end + + context 'when ENABLE_ACCOUNT_SIGNUP env variable is set to false' do it 'responds 404 on requests' do params = { account_name: 'test', email: email } ENV['ENABLE_ACCOUNT_SIGNUP'] = 'false' @@ -60,8 +90,6 @@ RSpec.describe 'Accounts API', type: :request do end context 'when ENABLE_ACCOUNT_SIGNUP env variable is set to api_only' do - let(:email) { Faker::Internet.email } - it 'does not respond 404 on requests' do params = { account_name: 'test', email: email } ENV['ENABLE_ACCOUNT_SIGNUP'] = 'api_only' diff --git a/spec/controllers/super_admin/access_tokens_controller_spec.rb b/spec/controllers/super_admin/access_tokens_controller_spec.rb new file mode 100644 index 000000000..c1f38ea23 --- /dev/null +++ b/spec/controllers/super_admin/access_tokens_controller_spec.rb @@ -0,0 +1,24 @@ +require 'rails_helper' + +RSpec.describe 'Super Admin access tokens API', type: :request do + let(:super_admin) { create(:super_admin) } + + describe 'GET /super_admin/access_tokens' do + context 'when it is an unauthenticated super admin' do + it 'returns unauthorized' do + get '/super_admin/' + expect(response).to have_http_status(:redirect) + end + end + + context 'when it is an authenticated super admin' do + it 'shows the list of access tokens' do + sign_in super_admin + get '/super_admin/access_tokens' + expect(response).to have_http_status(:success) + expect(response.body).to include('New access token') + expect(response.body).to include(super_admin.access_token.token) + end + end + end +end diff --git a/spec/controllers/super_admin/accounts_controller_spec.rb b/spec/controllers/super_admin/accounts_controller_spec.rb new file mode 100644 index 000000000..f4da5ad3d --- /dev/null +++ b/spec/controllers/super_admin/accounts_controller_spec.rb @@ -0,0 +1,26 @@ +require 'rails_helper' + +RSpec.describe 'Super Admin accounts API', type: :request do + let(:super_admin) { create(:super_admin) } + + describe 'GET /super_admin/accounts' do + context 'when it is an unauthenticated user' do + it 'returns unauthorized' do + get '/super_admin/accounts' + expect(response).to have_http_status(:redirect) + end + end + + context 'when it is an authenticated user' do + let!(:account) { create(:account) } + + it 'shows the list of accounts' do + sign_in super_admin + get '/super_admin/accounts' + expect(response).to have_http_status(:success) + expect(response.body).to include('New account') + expect(response.body).to include(account.name) + end + end + end +end diff --git a/spec/controllers/super_admin/super_admins_controller_spec.rb b/spec/controllers/super_admin/super_admins_controller_spec.rb new file mode 100644 index 000000000..0255b520c --- /dev/null +++ b/spec/controllers/super_admin/super_admins_controller_spec.rb @@ -0,0 +1,24 @@ +require 'rails_helper' + +RSpec.describe 'Super Admin super admins API', type: :request do + let(:super_admin) { create(:super_admin) } + + describe 'GET /super_admin/users' do + context 'when it is an unauthenticated super admin' do + it 'returns unauthorized' do + get '/super_admin/super_admins' + expect(response).to have_http_status(:redirect) + end + end + + context 'when it is an authenticated super admin' do + it 'shows the list of super admins' do + sign_in super_admin + get '/super_admin/super_admins' + expect(response).to have_http_status(:success) + expect(response.body).to include('New super admin') + expect(response.body).to include(super_admin.email) + end + end + end +end diff --git a/spec/controllers/super_admin/users_controller_spec.rb b/spec/controllers/super_admin/users_controller_spec.rb new file mode 100644 index 000000000..7a1385213 --- /dev/null +++ b/spec/controllers/super_admin/users_controller_spec.rb @@ -0,0 +1,26 @@ +require 'rails_helper' + +RSpec.describe 'Super Admin Users API', type: :request do + let(:super_admin) { create(:super_admin) } + + describe 'GET /super_admin/users' do + context 'when it is an unauthenticated super admin' do + it 'returns unauthorized' do + get '/super_admin/' + expect(response).to have_http_status(:redirect) + end + end + + context 'when it is an authenticated super admin' do + let!(:user) { create(:user) } + + it 'shows the list of users' do + sign_in super_admin + get '/super_admin' + expect(response).to have_http_status(:success) + expect(response.body).to include('New user') + expect(response.body).to include(user.name) + end + end + end +end diff --git a/spec/controllers/super_admin_controller_spec.rb b/spec/controllers/super_admin_controller_spec.rb new file mode 100644 index 000000000..ea0697dd9 --- /dev/null +++ b/spec/controllers/super_admin_controller_spec.rb @@ -0,0 +1,46 @@ +require 'rails_helper' + +RSpec.describe 'Super Admin', type: :request do + let(:super_admin) { create(:super_admin) } + + describe 'request to /super_admin' do + context 'when the super admin is unauthenticated' do + it 'redirects to signin page' do + get '/super_admin/' + expect(response).to have_http_status(:redirect) + expect(response.body).to include('sign_in') + end + + it 'signs super admin in and out' do + sign_in super_admin + get '/super_admin' + expect(response).to have_http_status(:success) + expect(response.body).to include('New user') + + sign_out super_admin + get '/super_admin' + expect(response).to have_http_status(:redirect) + end + end + end + + describe 'request to /super_admin/sidekiq' do + context 'when the super admin is unauthenticated' do + it 'redirects to signin page' do + get '/monitoring/sidekiq' + expect(response).to have_http_status(:not_found) + expect(response.body).to include('sign_in') + end + + it 'signs super admin in and out' do + sign_in super_admin + get '/monitoring/sidekiq' + expect(response).to have_http_status(:success) + + sign_out super_admin + get '/monitoring/sidekiq' + expect(response).to have_http_status(:not_found) + end + end + end +end diff --git a/spec/factories/super_admins.rb b/spec/factories/super_admins.rb new file mode 100644 index 000000000..88e488f68 --- /dev/null +++ b/spec/factories/super_admins.rb @@ -0,0 +1,6 @@ +FactoryBot.define do + factory :super_admin do + email { "admin@#{SecureRandom.uuid}.com" } + password { 'password' } + end +end diff --git a/spec/models/super_admin_spec.rb b/spec/models/super_admin_spec.rb new file mode 100644 index 000000000..e9a03b361 --- /dev/null +++ b/spec/models/super_admin_spec.rb @@ -0,0 +1,5 @@ +require 'rails_helper' + +RSpec.describe SuperAdmin, type: :model do + pending "add some examples to (or delete) #{__FILE__}" +end diff --git a/spec/rails_helper.rb b/spec/rails_helper.rb index 0c0d89421..ef39ac0ef 100644 --- a/spec/rails_helper.rb +++ b/spec/rails_helper.rb @@ -61,6 +61,8 @@ RSpec.configure do |config| config.filter_rails_from_backtrace! # arbitrary gems may also be filtered via: # config.filter_gems_from_backtrace("gem name") + + config.include Devise::Test::IntegrationHelpers, type: :request end Shoulda::Matchers.configure do |config| From f819bc0f33e31bf248c16eef89c2751d08637e91 Mon Sep 17 00:00:00 2001 From: Pranav Raj S Date: Tue, 12 May 2020 01:31:40 +0530 Subject: [PATCH 07/42] Chore: Use installation config in frontend (#847) * Use installation config in widget * Add configuration for installation in UI * Add config for mailer Co-authored-by: Sojan --- .../dashboard/components/layout/Sidebar.vue | 9 ++--- .../dashboard/components/ui/Wizard.vue | 11 ++++--- .../dashboard/routes/auth/Signup.vue | 31 +++++++++++++---- .../dashboard/settings/agents/Index.vue | 13 ++++++-- .../settings/inbox/InboxChannels.vue | 29 +++++++++++++++- .../routes/dashboard/settings/inbox/Index.vue | 13 ++++++-- .../settings/inbox/channels/Facebook.vue | 20 +++++++++-- .../dashboard/settings/integrations/Index.vue | 13 +++++--- .../settings/integrations/Webhook.vue | 12 ++++++- .../dashboard/routes/login/Login.vue | 19 +++++++---- app/javascript/dashboard/store/index.js | 6 ++-- .../shared/mixins/globalConfigMixin.js | 7 ++++ app/javascript/shared/store/globalConfig.js | 33 +++++++++++++++++++ app/javascript/widget/components/Branding.vue | 25 ++++++++++++-- app/javascript/widget/store/index.js | 2 ++ app/mailers/application_mailer.rb | 5 +++ app/views/layouts/mailer.html.erb | 6 ++-- app/views/layouts/vueapp.html.erb | 15 +++++---- app/views/widgets/show.html.erb | 1 + config/installation_config.yml | 16 +++++++-- public/brand-assets/logo.svg | 15 +++++++++ public/brand-assets/logo_thumbnail.svg | 12 +++++++ 22 files changed, 264 insertions(+), 49 deletions(-) create mode 100644 app/javascript/shared/mixins/globalConfigMixin.js create mode 100644 app/javascript/shared/store/globalConfig.js create mode 100644 public/brand-assets/logo.svg create mode 100644 public/brand-assets/logo_thumbnail.svg diff --git a/app/javascript/dashboard/components/layout/Sidebar.vue b/app/javascript/dashboard/components/layout/Sidebar.vue index dfb9081d2..a38ebe2cf 100644 --- a/app/javascript/dashboard/components/layout/Sidebar.vue +++ b/app/javascript/dashboard/components/layout/Sidebar.vue @@ -2,7 +2,7 @@
- +
-

{{ $t('INBOX_MGMT.ADD.FB.HELP') }}

+

+ {{ + useInstallationName( + $t('INBOX_MGMT.ADD.FB.HELP'), + globalConfig.installationName + ) + }} +

@@ -72,13 +84,14 @@ import { mapGetters } from 'vuex'; import ChannelApi from '../../../../../api/channels'; import PageHeader from '../../SettingsSubPageHeader'; import router from '../../../../index'; +import globalConfigMixin from 'shared/mixins/globalConfigMixin'; export default { components: { LoadingState, PageHeader, }, - + mixins: [globalConfigMixin], data() { return { isCreating: false, @@ -114,6 +127,7 @@ export default { }, ...mapGetters({ currentUser: 'getCurrentUser', + globalConfig: 'globalConfig/get', }), accountId() { return this.currentUser.account_id; diff --git a/app/javascript/dashboard/routes/dashboard/settings/integrations/Index.vue b/app/javascript/dashboard/routes/dashboard/settings/integrations/Index.vue index 4ebad6b2d..7eb506273 100644 --- a/app/javascript/dashboard/routes/dashboard/settings/integrations/Index.vue +++ b/app/javascript/dashboard/routes/dashboard/settings/integrations/Index.vue @@ -13,7 +13,12 @@ {{ $t('INTEGRATION_SETTINGS.WEBHOOK.TITLE') }}

- {{ $t('INTEGRATION_SETTINGS.WEBHOOK.INTEGRATION_TXT') }} + {{ + useInstallationName( + $t('INTEGRATION_SETTINGS.WEBHOOK.INTEGRATION_TXT'), + globalConfig.installationName + ) + }}

@@ -33,20 +38,20 @@
- + diff --git a/app/javascript/widget/components/AgentMessageBubble.vue b/app/javascript/widget/components/AgentMessageBubble.vue index cc2044980..1062614da 100755 --- a/app/javascript/widget/components/AgentMessageBubble.vue +++ b/app/javascript/widget/components/AgentMessageBubble.vue @@ -21,7 +21,7 @@ { @@ -25,6 +26,34 @@ export const createTemporaryMessage = ({ attachments, content }) => { }; }; +const getSenderName = message => (message.sender ? message.sender.name : ''); + +const shouldShowAvatar = (message, nextMessage) => { + const currentSender = getSenderName(message); + const nextSender = getSenderName(nextMessage); + + return ( + currentSender !== nextSender || + message.message_type !== nextMessage.message_type || + isASubmittedFormMessage(nextMessage) + ); +}; + +const groupConversationBySender = conversationsForADate => + conversationsForADate.map((message, index) => { + let showAvatar = false; + const isLastMessage = index === conversationsForADate.length - 1; + if (isASubmittedFormMessage(message)) { + showAvatar = false; + } else if (isLastMessage) { + showAvatar = true; + } else { + const nextMessage = conversationsForADate[index + 1]; + showAvatar = shouldShowAvatar(message, nextMessage); + } + return { showAvatar, ...message }; + }); + export const findUndeliveredMessage = (messageInbox, { content }) => Object.values(messageInbox).filter( message => message.content === content && message.status === 'in_progress' @@ -58,27 +87,10 @@ export const getters = { Object.values(_state.conversations), message => new DateHelper(message.created_at).format() ); - return Object.keys(conversationGroupedByDate).map(date => { - const messages = conversationGroupedByDate[date].map((message, index) => { - let showAvatar = false; - if (index === conversationGroupedByDate[date].length - 1) { - showAvatar = true; - } else { - const nextMessage = conversationGroupedByDate[date][index + 1]; - const currentSender = message.sender ? message.sender.name : ''; - const nextSender = nextMessage.sender ? nextMessage.sender.name : ''; - showAvatar = - currentSender !== nextSender || - message.message_type !== nextMessage.message_type; - } - return { showAvatar, ...message }; - }); - - return { - date, - messages, - }; - }); + return Object.keys(conversationGroupedByDate).map(date => ({ + date, + messages: groupConversationBySender(conversationGroupedByDate[date]), + })); }, getIsFetchingList: _state => _state.uiFlags.isFetchingList, }; diff --git a/app/javascript/widget/store/modules/specs/conversation/getters.spec.js b/app/javascript/widget/store/modules/specs/conversation/getters.spec.js index 91bc7396b..99f1c983a 100644 --- a/app/javascript/widget/store/modules/specs/conversation/getters.spec.js +++ b/app/javascript/widget/store/modules/specs/conversation/getters.spec.js @@ -56,40 +56,41 @@ describe('#getters', () => { expect(getters.getIsAgentTyping(state)).toEqual(false); }); - it('uiFlags', () => { - const state = { - conversations: { - 1: { - id: 1, - content: 'Thanks for the help', - created_at: 1574075964, - message_type: 0, + it('getGroupedConversation', () => { + expect( + getters.getGroupedConversation({ + conversations: { + 1: { + id: 1, + content: 'Thanks for the help', + created_at: 1574075964, + message_type: 0, + }, + 2: { + id: 2, + content: 'Yes, It makes sense', + created_at: 1574092218, + message_type: 0, + }, + 3: { + id: 3, + content: 'Hey', + created_at: 1574092218, + message_type: 1, + }, + 4: { + id: 4, + content: 'Hey', + created_at: 1576340623, + }, + 5: { + id: 5, + content: 'How may I help you', + created_at: 1576340626, + }, }, - 2: { - id: 2, - content: 'Yes, It makes sense', - created_at: 1574092218, - message_type: 0, - }, - 3: { - id: 3, - content: 'Hey', - created_at: 1574092218, - message_type: 1, - }, - 4: { - id: 4, - content: 'Hey', - created_at: 1576340623, - }, - 5: { - id: 5, - content: 'How may I help you', - created_at: 1576340626, - }, - }, - }; - expect(getters.getGroupedConversation(state)).toEqual([ + }) + ).toEqual([ { date: 'Nov 18, 2019', messages: [ @@ -134,5 +135,131 @@ describe('#getters', () => { ], }, ]); + + expect( + getters.getGroupedConversation({ + conversations: { + 1: { + id: 1, + content: 'Thanks for the help', + created_at: 1574075964, + message_type: 0, + }, + 2: { + id: 2, + content: 'Yes, It makes sense', + created_at: 1574092218, + message_type: 0, + }, + 3: { + id: 3, + content: 'Hey', + created_at: 1574092218, + message_type: 1, + }, + 4: { + id: 4, + content: 'Hey', + created_at: 1576340623, + }, + 5: { + id: 5, + content: 'How may I help you', + created_at: 1576340626, + message_type: 2, + content_type: 'form', + content_attributes: { + submitted_values: [{ name: 'text', value: 'sample text' }], + }, + }, + 6: { + id: 6, + content: 'How may I help you', + created_at: 1576340626, + message_type: 2, + content_type: 'form', + }, + 7: { + id: 7, + content: 'How may I help you', + created_at: 1576340626, + message_type: 2, + content_type: 'form', + content_attributes: { + submitted_values: [{ name: 'text', value: 'sample text' }], + }, + }, + }, + }) + ).toEqual([ + { + date: 'Nov 18, 2019', + messages: [ + { + id: 1, + content: 'Thanks for the help', + created_at: 1574075964, + showAvatar: false, + message_type: 0, + }, + { + id: 2, + content: 'Yes, It makes sense', + created_at: 1574092218, + showAvatar: true, + message_type: 0, + }, + { + id: 3, + content: 'Hey', + created_at: 1574092218, + showAvatar: true, + message_type: 1, + }, + ], + }, + { + date: 'Dec 14, 2019', + messages: [ + { + id: 4, + content: 'Hey', + created_at: 1576340623, + showAvatar: true, + }, + { + id: 5, + content: 'How may I help you', + created_at: 1576340626, + message_type: 2, + content_type: 'form', + content_attributes: { + submitted_values: [{ name: 'text', value: 'sample text' }], + }, + showAvatar: false, + }, + { + id: 6, + content: 'How may I help you', + created_at: 1576340626, + message_type: 2, + content_type: 'form', + showAvatar: true, + }, + { + id: 7, + content: 'How may I help you', + created_at: 1576340626, + message_type: 2, + content_type: 'form', + content_attributes: { + submitted_values: [{ name: 'text', value: 'sample text' }], + }, + + showAvatar: false, + }, + ], + }, + ]); }); }); From 6e92d9be9ef5b110d3b09dd1bdadf60704f323a3 Mon Sep 17 00:00:00 2001 From: Pranav Raj S Date: Sun, 17 May 2020 23:44:50 +0530 Subject: [PATCH 19/42] Bug: Fix Facebook v7.0 API issues (#863) * Fix v7.0 API issues Co-authored-by: Sojan --- Gemfile.lock | 30 +++++++++---------- .../widgets/conversation/ReplyBox.vue | 4 ++- .../dashboard/i18n/locale/en/inboxMgmt.json | 8 ++++- .../settings/inbox/channels/Facebook.vue | 23 +++++++------- app/models/channel/facebook_page.rb | 15 +++++++++- app/models/inbox.rb | 13 -------- 6 files changed, 48 insertions(+), 45 deletions(-) diff --git a/Gemfile.lock b/Gemfile.lock index cc6d481ee..7c3079978 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -96,16 +96,16 @@ GEM autoprefixer-rails (9.7.6) execjs aws-eventstream (1.1.0) - aws-partitions (1.310.0) - aws-sdk-core (3.94.1) + aws-partitions (1.315.0) + aws-sdk-core (3.95.0) aws-eventstream (~> 1, >= 1.0.2) aws-partitions (~> 1, >= 1.239.0) aws-sigv4 (~> 1.1) jmespath (~> 1.0) - aws-sdk-kms (1.30.0) + aws-sdk-kms (1.31.0) aws-sdk-core (~> 3, >= 3.71.0) aws-sigv4 (~> 1.1) - aws-sdk-s3 (1.63.1) + aws-sdk-s3 (1.64.0) aws-sdk-core (~> 3, >= 3.83.0) aws-sdk-kms (~> 1) aws-sigv4 (~> 1.1) @@ -126,8 +126,8 @@ GEM bindex (0.8.1) bootsnap (1.4.6) msgpack (~> 1.0) - brakeman (4.8.1) - browser (4.0.0) + brakeman (4.8.2) + browser (4.1.0) builder (3.2.4) bullet (6.1.0) activesupport (>= 3.0.0) @@ -194,7 +194,7 @@ GEM foreman (0.87.1) globalid (0.4.2) activesupport (>= 4.2.0) - google-api-client (0.38.0) + google-api-client (0.39.3) addressable (~> 2.5, >= 2.5.1) googleauth (~> 0.9) httpclient (>= 2.8.1, < 3.0) @@ -208,7 +208,7 @@ GEM google-cloud-env (1.3.1) faraday (>= 0.17.3, < 2.0) google-cloud-errors (1.0.0) - google-cloud-storage (1.26.0) + google-cloud-storage (1.26.1) addressable (~> 2.5) digest-crc (~> 0.4) google-api-client (~> 0.33) @@ -239,11 +239,10 @@ GEM concurrent-ruby (~> 1.0) ice_nine (0.11.2) inflecto (0.0.2) - jaro_winkler (1.5.4) jbuilder (2.10.0) activesupport (>= 5.0.0) jmespath (1.4.0) - jquery-rails (4.3.5) + jquery-rails (4.4.0) rails-dom-testing (>= 1, < 3) railties (>= 4.2.0) thor (>= 0.14, < 2.0) @@ -284,7 +283,7 @@ GEM method_source (1.0.0) mime-types (3.3.1) mime-types-data (~> 3.2015) - mime-types-data (3.2020.0425) + mime-types-data (3.2020.0512) mimemagic (0.3.5) mini_magick (4.10.1) mini_mime (1.0.2) @@ -312,7 +311,7 @@ GEM method_source (~> 1.0) pry-rails (0.3.9) pry (>= 0.10.4) - public_suffix (4.0.4) + public_suffix (4.0.5) puma (4.3.3) nio4r (~> 2.0) pundit (2.1.0) @@ -383,7 +382,7 @@ GEM rexml (3.2.4) rspec-core (3.9.2) rspec-support (~> 3.9.3) - rspec-expectations (3.9.1) + rspec-expectations (3.9.2) diff-lcs (>= 1.2.0, < 2.0) rspec-support (~> 3.9.0) rspec-mocks (3.9.1) @@ -398,8 +397,7 @@ GEM rspec-mocks (~> 3.9) rspec-support (~> 3.9) rspec-support (3.9.3) - rubocop (0.82.0) - jaro_winkler (~> 1.5.1) + rubocop (0.83.0) parallel (~> 1.10) parser (>= 2.7.0.1) rainbow (>= 2.2.2, < 4.0) @@ -471,7 +469,7 @@ GEM faraday inflecto virtus - telephone_number (1.4.6) + telephone_number (1.4.7) thor (0.20.3) thread_safe (0.3.6) tilt (2.0.10) diff --git a/app/javascript/dashboard/components/widgets/conversation/ReplyBox.vue b/app/javascript/dashboard/components/widgets/conversation/ReplyBox.vue index 4f73291b2..8a2a836ba 100644 --- a/app/javascript/dashboard/components/widgets/conversation/ReplyBox.vue +++ b/app/javascript/dashboard/components/widgets/conversation/ReplyBox.vue @@ -208,7 +208,9 @@ export default { async sendMessage() { const isMessageEmpty = !this.message.replace(/\n/g, '').length; if (isMessageEmpty) return; - + if (this.message.length > this.maxLength) { + return; + } if (!this.showCannedResponsesList) { try { await this.$store.dispatch('sendMessage', { diff --git a/app/javascript/dashboard/i18n/locale/en/inboxMgmt.json b/app/javascript/dashboard/i18n/locale/en/inboxMgmt.json index aa43c5ecb..2aa1c15c2 100644 --- a/app/javascript/dashboard/i18n/locale/en/inboxMgmt.json +++ b/app/javascript/dashboard/i18n/locale/en/inboxMgmt.json @@ -29,7 +29,13 @@ ], "ADD": { "FB": { - "HELP": "PS: By signing in, we only get access to your Page's messages. Your private messages can never be accessed by Chatwoot." + "HELP": "PS: By signing in, we only get access to your Page's messages. Your private messages can never be accessed by Chatwoot.", + "CHOOSE_PAGE": "Choose Page", + "CHOOSE_PLACEHOLDER": "Select a page from the list", + "INBOX_NAME": "Inbox Name", + "ADD_NAME": "Add a name for your inbox", + "PICK_NAME": "Pick A Name Your Inbox", + "PICK_A_VALUE": "Pick a value" }, "TWITTER": { "HELP": "To add your Twitter profile as a channel, you need to authenticate your Twitter Profile by clicking on 'Sign in with Twitter' " diff --git a/app/javascript/dashboard/routes/dashboard/settings/inbox/channels/Facebook.vue b/app/javascript/dashboard/routes/dashboard/settings/inbox/channels/Facebook.vue index 25634d026..282096616 100644 --- a/app/javascript/dashboard/routes/dashboard/settings/inbox/channels/Facebook.vue +++ b/app/javascript/dashboard/routes/dashboard/settings/inbox/channels/Facebook.vue @@ -17,10 +17,7 @@

- +
- Choose Page + {{ $t('INBOX_MGMT.ADD.FB.CHOOSE_PAGE') }} - Select a page from the list + {{ $t('INBOX_MGMT.ADD.FB.CHOOSE_PLACEHOLDER') }}
@@ -166,7 +163,7 @@ export default { FB.init({ appId: window.chatwootConfig.fbAppId, xfbml: true, - version: 'v4.0', + version: 'v7.0', status: true, }); window.fbSDKLoaded = true; @@ -209,7 +206,7 @@ export default { } }, { - scope: 'manage_pages,pages_messaging,pages_messaging_phone_number', + scope: 'pages_manage_metadata,pages_messaging', } ); }, diff --git a/app/models/channel/facebook_page.rb b/app/models/channel/facebook_page.rb index 2b5800113..7c087bda9 100644 --- a/app/models/channel/facebook_page.rb +++ b/app/models/channel/facebook_page.rb @@ -28,13 +28,26 @@ class Channel::FacebookPage < ApplicationRecord has_one :inbox, as: :channel, dependent: :destroy + after_create_commit :subscribe before_destroy :unsubscribe - private + def subscribe + # ref https://developers.facebook.com/docs/messenger-platform/reference/webhook-events + response = Facebook::Messenger::Subscriptions.subscribe( + access_token: page_access_token, + subscribed_fields: %w[ + messages message_deliveries message_echoes message_reads + ] + ) + rescue => e + Rails.logger.debug "Rescued: #{e.inspect}" + true + end def unsubscribe Facebook::Messenger::Subscriptions.unsubscribe(access_token: page_access_token) rescue => e + Rails.logger.debug "Rescued: #{e.inspect}" true end end diff --git a/app/models/inbox.rb b/app/models/inbox.rb index 0ce3b251f..4fb395ba5 100644 --- a/app/models/inbox.rb +++ b/app/models/inbox.rb @@ -41,7 +41,6 @@ class Inbox < ApplicationRecord has_one :agent_bot, through: :agent_bot_inbox has_many :webhooks, dependent: :destroy - after_create :subscribe_webhook, if: :facebook? after_destroy :delete_round_robin_agents def add_member(user_id) @@ -83,16 +82,4 @@ class Inbox < ApplicationRecord def round_robin_key format(Constants::RedisKeys::ROUND_ROBIN_AGENTS, inbox_id: id) end - - def subscribe_webhook - Facebook::Messenger::Subscriptions.subscribe( - access_token: channel.page_access_token, - subscribed_fields: %w[ - messages messaging_postbacks messaging_optins message_deliveries - message_reads messaging_payments messaging_pre_checkouts messaging_checkout_updates - messaging_account_linking messaging_referrals message_echoes messaging_game_plays - standby messaging_handovers messaging_policy_enforcement message_reactions - ] - ) - end end From 3f5ce2ddbf66beff9beb7be121ff7f0f975edb5a Mon Sep 17 00:00:00 2001 From: Pranav Raj S Date: Mon, 18 May 2020 15:32:26 +0530 Subject: [PATCH 20/42] Bug: Fix autoload_paths for facebook bot (#877) * Fix autoload_paths for facebook bot --- app/bot/bot.rb | 19 ---------------- app/bot/facebook_bot.rb | 22 +++++++++++++++++++ app/services/facebook/send_reply_service.rb | 2 +- config/application.rb | 2 +- .../facebook/send_reply_service_spec.rb | 2 +- 5 files changed, 25 insertions(+), 22 deletions(-) delete mode 100644 app/bot/bot.rb create mode 100644 app/bot/facebook_bot.rb diff --git a/app/bot/bot.rb b/app/bot/bot.rb deleted file mode 100644 index c834e7774..000000000 --- a/app/bot/bot.rb +++ /dev/null @@ -1,19 +0,0 @@ -# app/bot/facebook_bot.rb -require 'facebook/messenger' -include Facebook::Messenger - -Bot.on :message do |message| - response = ::Integrations::Facebook::MessageParser.new(message) - ::Integrations::Facebook::MessageCreator.new(response).perform -end - -Bot.on :delivery do |delivery| - # delivery.ids # => 'mid.1457764197618:41d102a3e1ae206a38' - # delivery.sender # => { 'id' => '1008372609250235' } - # delivery.recipient # => { 'id' => '2015573629214912' } - # delivery.at # => 2016-04-22 21:30:36 +0200 - # delivery.seq # => 37 - updater = Integrations::Facebook::DeliveryStatus.new(delivery) - updater.perform - puts "Human was online at #{delivery.at}" -end diff --git a/app/bot/facebook_bot.rb b/app/bot/facebook_bot.rb new file mode 100644 index 000000000..f0e04ebc2 --- /dev/null +++ b/app/bot/facebook_bot.rb @@ -0,0 +1,22 @@ +require 'facebook/messenger' + +class FacebookBot + include Facebook::Messenger + + Bot.on :message do |message| + Rails.logger.info "MESSAGE_RECIEVED #{message}" + response = ::Integrations::Facebook::MessageParser.new(message) + ::Integrations::Facebook::MessageCreator.new(response).perform + end + + Bot.on :delivery do |delivery| + # delivery.ids # => 'mid.1457764197618:41d102a3e1ae206a38' + # delivery.sender # => { 'id' => '1008372609250235' } + # delivery.recipient # => { 'id' => '2015573629214912' } + # delivery.at # => 2016-04-22 21:30:36 +0200 + # delivery.seq # => 37 + updater = Integrations::Facebook::DeliveryStatus.new(delivery) + updater.perform + Rails.logger.info "Human was online at #{delivery.at}" + end +end diff --git a/app/services/facebook/send_reply_service.rb b/app/services/facebook/send_reply_service.rb index df897a1f2..9173ecac7 100644 --- a/app/services/facebook/send_reply_service.rb +++ b/app/services/facebook/send_reply_service.rb @@ -6,7 +6,7 @@ class Facebook::SendReplyService return if inbox.channel.class.to_s != 'Channel::FacebookPage' return unless outgoing_message_from_chatwoot? - Bot.deliver(delivery_params, access_token: message.channel_token) + FacebookBot::Bot.deliver(delivery_params, access_token: message.channel_token) end private diff --git a/config/application.rb b/config/application.rb index c7421cb4b..436a9392f 100644 --- a/config/application.rb +++ b/config/application.rb @@ -18,7 +18,7 @@ module Chatwoot # This is required in production for zeitwerk to autoload the file config.paths.add File.join('app', 'bot'), glob: File.join('**', '*.rb') - config.autoload_paths << Rails.root.join('app/bot') + config.autoload_paths << Rails.root.join('app/bot/*') # Settings in config/environments/* take precedence over those specified here. # Application configuration can go into files in config/initializers diff --git a/spec/services/facebook/send_reply_service_spec.rb b/spec/services/facebook/send_reply_service_spec.rb index 113b091e0..a74f1e826 100644 --- a/spec/services/facebook/send_reply_service_spec.rb +++ b/spec/services/facebook/send_reply_service_spec.rb @@ -9,7 +9,7 @@ describe Facebook::SendReplyService do end let!(:account) { create(:account) } - let(:bot) { class_double('Bot').as_stubbed_const } + let(:bot) { class_double('FacebookBot::Bot').as_stubbed_const } let!(:widget_inbox) { create(:inbox, account: account) } let!(:facebook_channel) { create(:channel_facebook_page, account: account) } let!(:facebook_inbox) { create(:inbox, channel: facebook_channel, account: account) } From 8af200ad209cd35a6a4d85246d21691efe193d47 Mon Sep 17 00:00:00 2001 From: Pranav Raj S Date: Tue, 19 May 2020 19:05:10 +0530 Subject: [PATCH 21/42] Fix missing translations, enable dutch language (#878) * Fix missing translations, enable dutch language --- Gemfile.lock | 116 +++++++++--------- app/javascript/dashboard/i18n/index.js | 8 +- .../dashboard/i18n/locale/nl/index.js | 34 +++++ .../widget/components/AvailableAgents.vue | 5 +- .../widget/helpers/specs/utils.spec.js | 26 ---- app/javascript/widget/helpers/utils.js | 17 --- app/javascript/widget/i18n/locale/ar.json | 6 + app/javascript/widget/i18n/locale/ca.json | 6 + app/javascript/widget/i18n/locale/de.json | 6 + app/javascript/widget/i18n/locale/el.json | 6 + app/javascript/widget/i18n/locale/en.json | 6 + app/javascript/widget/i18n/locale/fr.json | 6 + app/javascript/widget/i18n/locale/nl.json | 6 + app/javascript/widget/mixins/agentMixin.js | 24 ++++ .../widget/mixins/specs/agentMixin.spec.js | 49 ++++++++ config/initializers/languages.rb | 2 +- 16 files changed, 216 insertions(+), 107 deletions(-) create mode 100644 app/javascript/dashboard/i18n/locale/nl/index.js delete mode 100644 app/javascript/widget/helpers/specs/utils.spec.js create mode 100644 app/javascript/widget/mixins/agentMixin.js create mode 100644 app/javascript/widget/mixins/specs/agentMixin.spec.js diff --git a/Gemfile.lock b/Gemfile.lock index 7c3079978..cbc3d1f6c 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -18,56 +18,56 @@ GEM specs: action-cable-testing (0.6.1) actioncable (>= 5.0) - actioncable (6.0.3) - actionpack (= 6.0.3) + actioncable (6.0.3.1) + actionpack (= 6.0.3.1) nio4r (~> 2.0) websocket-driver (>= 0.6.1) - actionmailbox (6.0.3) - actionpack (= 6.0.3) - activejob (= 6.0.3) - activerecord (= 6.0.3) - activestorage (= 6.0.3) - activesupport (= 6.0.3) + actionmailbox (6.0.3.1) + actionpack (= 6.0.3.1) + activejob (= 6.0.3.1) + activerecord (= 6.0.3.1) + activestorage (= 6.0.3.1) + activesupport (= 6.0.3.1) mail (>= 2.7.1) - actionmailer (6.0.3) - actionpack (= 6.0.3) - actionview (= 6.0.3) - activejob (= 6.0.3) + actionmailer (6.0.3.1) + actionpack (= 6.0.3.1) + actionview (= 6.0.3.1) + activejob (= 6.0.3.1) mail (~> 2.5, >= 2.5.4) rails-dom-testing (~> 2.0) - actionpack (6.0.3) - actionview (= 6.0.3) - activesupport (= 6.0.3) + actionpack (6.0.3.1) + actionview (= 6.0.3.1) + activesupport (= 6.0.3.1) rack (~> 2.0, >= 2.0.8) rack-test (>= 0.6.3) rails-dom-testing (~> 2.0) rails-html-sanitizer (~> 1.0, >= 1.2.0) - actiontext (6.0.3) - actionpack (= 6.0.3) - activerecord (= 6.0.3) - activestorage (= 6.0.3) - activesupport (= 6.0.3) + actiontext (6.0.3.1) + actionpack (= 6.0.3.1) + activerecord (= 6.0.3.1) + activestorage (= 6.0.3.1) + activesupport (= 6.0.3.1) nokogiri (>= 1.8.5) - actionview (6.0.3) - activesupport (= 6.0.3) + actionview (6.0.3.1) + activesupport (= 6.0.3.1) builder (~> 3.1) erubi (~> 1.4) rails-dom-testing (~> 2.0) rails-html-sanitizer (~> 1.1, >= 1.2.0) - activejob (6.0.3) - activesupport (= 6.0.3) + activejob (6.0.3.1) + activesupport (= 6.0.3.1) globalid (>= 0.3.6) - activemodel (6.0.3) - activesupport (= 6.0.3) - activerecord (6.0.3) - activemodel (= 6.0.3) - activesupport (= 6.0.3) - activestorage (6.0.3) - actionpack (= 6.0.3) - activejob (= 6.0.3) - activerecord (= 6.0.3) + activemodel (6.0.3.1) + activesupport (= 6.0.3.1) + activerecord (6.0.3.1) + activemodel (= 6.0.3.1) + activesupport (= 6.0.3.1) + activestorage (6.0.3.1) + actionpack (= 6.0.3.1) + activejob (= 6.0.3.1) + activerecord (= 6.0.3.1) marcel (~> 0.3.1) - activesupport (6.0.3) + activesupport (6.0.3.1) concurrent-ruby (~> 1.0, >= 1.0.2) i18n (>= 0.7, < 2) minitest (~> 5.1) @@ -96,8 +96,8 @@ GEM autoprefixer-rails (9.7.6) execjs aws-eventstream (1.1.0) - aws-partitions (1.315.0) - aws-sdk-core (3.95.0) + aws-partitions (1.317.0) + aws-sdk-core (3.96.1) aws-eventstream (~> 1, >= 1.0.2) aws-partitions (~> 1, >= 1.239.0) aws-sigv4 (~> 1.1) @@ -105,8 +105,8 @@ GEM aws-sdk-kms (1.31.0) aws-sdk-core (~> 3, >= 3.71.0) aws-sigv4 (~> 1.1) - aws-sdk-s3 (1.64.0) - aws-sdk-core (~> 3, >= 3.83.0) + aws-sdk-s3 (1.65.0) + aws-sdk-core (~> 3, >= 3.96.1) aws-sdk-kms (~> 1) aws-sigv4 (~> 1.1) aws-sigv4 (1.1.3) @@ -194,7 +194,7 @@ GEM foreman (0.87.1) globalid (0.4.2) activesupport (>= 4.2.0) - google-api-client (0.39.3) + google-api-client (0.39.4) addressable (~> 2.5, >= 2.5.1) googleauth (~> 0.9) httpclient (>= 2.8.1, < 3.0) @@ -288,7 +288,7 @@ GEM mini_magick (4.10.1) mini_mime (1.0.2) mini_portile2 (2.4.0) - minitest (5.14.0) + minitest (5.14.1) momentjs-rails (2.20.1) railties (>= 3.1) msgpack (1.3.3) @@ -312,7 +312,7 @@ GEM pry-rails (0.3.9) pry (>= 0.10.4) public_suffix (4.0.5) - puma (4.3.3) + puma (4.3.4) nio4r (~> 2.0) pundit (2.1.0) activesupport (>= 3.0.0) @@ -327,29 +327,29 @@ GEM rack rack-test (1.1.0) rack (>= 1.0, < 3) - rails (6.0.3) - actioncable (= 6.0.3) - actionmailbox (= 6.0.3) - actionmailer (= 6.0.3) - actionpack (= 6.0.3) - actiontext (= 6.0.3) - actionview (= 6.0.3) - activejob (= 6.0.3) - activemodel (= 6.0.3) - activerecord (= 6.0.3) - activestorage (= 6.0.3) - activesupport (= 6.0.3) + rails (6.0.3.1) + actioncable (= 6.0.3.1) + actionmailbox (= 6.0.3.1) + actionmailer (= 6.0.3.1) + actionpack (= 6.0.3.1) + actiontext (= 6.0.3.1) + actionview (= 6.0.3.1) + activejob (= 6.0.3.1) + activemodel (= 6.0.3.1) + activerecord (= 6.0.3.1) + activestorage (= 6.0.3.1) + activesupport (= 6.0.3.1) bundler (>= 1.3.0) - railties (= 6.0.3) + railties (= 6.0.3.1) sprockets-rails (>= 2.0.0) rails-dom-testing (2.0.3) activesupport (>= 4.2.0) nokogiri (>= 1.6) rails-html-sanitizer (1.3.0) loofah (~> 2.3) - railties (6.0.3) - actionpack (= 6.0.3) - activesupport (= 6.0.3) + railties (6.0.3.1) + actionpack (= 6.0.3.1) + activesupport (= 6.0.3.1) method_source rake (>= 0.8.7) thor (>= 0.20.3, < 2.0) @@ -388,7 +388,7 @@ GEM rspec-mocks (3.9.1) diff-lcs (>= 1.2.0, < 2.0) rspec-support (~> 3.9.0) - rspec-rails (4.0.0) + rspec-rails (4.0.1) actionpack (>= 4.2) activesupport (>= 4.2) railties (>= 4.2) diff --git a/app/javascript/dashboard/i18n/index.js b/app/javascript/dashboard/i18n/index.js index 2d34fb255..5840d00c3 100644 --- a/app/javascript/dashboard/i18n/index.js +++ b/app/javascript/dashboard/i18n/index.js @@ -1,12 +1,13 @@ import ca from './locale/ca'; -import ro from './locale/ro'; -import fr from './locale/fr'; -import pt_BR from './locale/pt_BR'; import de from './locale/de'; import el from './locale/el'; import en from './locale/en'; +import fr from './locale/fr'; +import nl from './locale/nl'; import ml from './locale/ml'; import pt from './locale/pt'; +import pt_BR from './locale/pt_BR'; +import ro from './locale/ro'; export default { ca, @@ -14,6 +15,7 @@ export default { el, en, fr, + nl, ml, pt_BR, pt, diff --git a/app/javascript/dashboard/i18n/locale/nl/index.js b/app/javascript/dashboard/i18n/locale/nl/index.js new file mode 100644 index 000000000..351906dd9 --- /dev/null +++ b/app/javascript/dashboard/i18n/locale/nl/index.js @@ -0,0 +1,34 @@ +/* eslint-disable */ +import { default as _agentMgmt } from './agentMgmt.json'; +import { default as _billing } from './billing.json'; +import { default as _cannedMgmt } from './cannedMgmt.json'; +import { default as _chatlist } from './chatlist.json'; +import { default as _contact } from './contact.json'; +import { default as _conversation } from './conversation.json'; +import { default as _inboxMgmt } from './inboxMgmt.json'; +import { default as _login } from './login.json'; +import { default as _report } from './report.json'; +import { default as _resetPassword } from './resetPassword.json'; +import { default as _setNewPassword } from './setNewPassword.json'; +import { default as _settings } from './settings.json'; +import { default as _signup } from './signup.json'; +import { default as _integrations } from './integrations.json'; +import { default as _generalSettings } from './generalSettings.json'; + +export default { + ..._agentMgmt, + ..._billing, + ..._cannedMgmt, + ..._chatlist, + ..._contact, + ..._conversation, + ..._inboxMgmt, + ..._login, + ..._report, + ..._resetPassword, + ..._setNewPassword, + ..._settings, + ..._signup, + ..._integrations, + ..._generalSettings, +}; diff --git a/app/javascript/widget/components/AvailableAgents.vue b/app/javascript/widget/components/AvailableAgents.vue index b9c8f06eb..060c9373d 100644 --- a/app/javascript/widget/components/AvailableAgents.vue +++ b/app/javascript/widget/components/AvailableAgents.vue @@ -13,11 +13,12 @@ @@ -248,4 +258,10 @@ export default { padding: 0.2rem; } } + +.contact--mute { + color: $alert-color; + display: block; + text-align: center; +} diff --git a/app/javascript/dashboard/store/modules/conversations/actions.js b/app/javascript/dashboard/store/modules/conversations/actions.js index b06312e0e..683e2a0f7 100644 --- a/app/javascript/dashboard/store/modules/conversations/actions.js +++ b/app/javascript/dashboard/store/modules/conversations/actions.js @@ -215,6 +215,15 @@ const actions = { // Handle error } }, + + muteConversation: async ({ commit }, conversationId) => { + try { + await ConversationApi.mute(conversationId); + commit(types.default.MUTE_CONVERSATION); + } catch (error) { + // + } + }, }; export default actions; diff --git a/app/javascript/dashboard/store/modules/conversations/index.js b/app/javascript/dashboard/store/modules/conversations/index.js index 9c9a0f22b..d016a55ef 100644 --- a/app/javascript/dashboard/store/modules/conversations/index.js +++ b/app/javascript/dashboard/store/modules/conversations/index.js @@ -10,6 +10,7 @@ const initialSelectedChat = { id: null, meta: {}, status: null, + muted: false, seen: false, agentTyping: 'off', dataFetched: false, @@ -116,6 +117,12 @@ const mutations = { _state.selectedChat.status = status; }, + [types.default.MUTE_CONVERSATION](_state) { + const [chat] = getSelectedChatConversation(_state); + chat.muted = true; + _state.selectedChat.muted = true; + }, + [types.default.SEND_MESSAGE](_state, currentMessage) { const [chat] = getSelectedChatConversation(_state); const allMessagesExceptCurrent = (chat.messages || []).filter( diff --git a/app/javascript/dashboard/store/modules/specs/conversations/actions.spec.js b/app/javascript/dashboard/store/modules/specs/conversations/actions.spec.js index 306b87bbb..7898822aa 100644 --- a/app/javascript/dashboard/store/modules/specs/conversations/actions.spec.js +++ b/app/javascript/dashboard/store/modules/specs/conversations/actions.spec.js @@ -21,4 +21,16 @@ describe('#actions', () => { expect(commit.mock.calls).toEqual([]); }); }); + describe('#muteConversation', () => { + it('sends correct actions if API is success', async () => { + axios.get.mockResolvedValue(null); + await actions.muteConversation({ commit }, 1); + expect(commit.mock.calls).toEqual([[types.default.MUTE_CONVERSATION]]); + }); + it('sends correct actions if API is error', async () => { + axios.get.mockRejectedValue({ message: 'Incorrect header' }); + await actions.getConversation({ commit }); + expect(commit.mock.calls).toEqual([]); + }); + }); }); diff --git a/app/javascript/dashboard/store/mutation-types.js b/app/javascript/dashboard/store/mutation-types.js index e6ee84775..d2df7aa7d 100755 --- a/app/javascript/dashboard/store/mutation-types.js +++ b/app/javascript/dashboard/store/mutation-types.js @@ -22,6 +22,7 @@ export default { RESOLVE_CONVERSATION: 'RESOLVE_CONVERSATION', ADD_CONVERSATION: 'ADD_CONVERSATION', UPDATE_CONVERSATION: 'UPDATE_CONVERSATION', + MUTE_CONVERSATION: 'MUTE_CONVERSATION', SEND_MESSAGE: 'SEND_MESSAGE', ASSIGN_AGENT: 'ASSIGN_AGENT', SET_CHAT_META: 'SET_CHAT_META', diff --git a/app/models/conversation.rb b/app/models/conversation.rb index d72202a98..59e9a853f 100644 --- a/app/models/conversation.rb +++ b/app/models/conversation.rb @@ -74,6 +74,15 @@ class Conversation < ApplicationRecord save end + def mute! + resolved! + Redis::Alfred.setex(mute_key, 1, mute_period) + end + + def muted? + !Redis::Alfred.get(mute_key).nil? + end + def lock! update!(locked: true) end @@ -184,4 +193,12 @@ class Conversation < ApplicationRecord messages.create(activity_message_params(content)) end + + def mute_key + format('CONVERSATION::%d::MUTED', id: id) + end + + def mute_period + 6.hours + end end diff --git a/app/models/message.rb b/app/models/message.rb index 47ea8c425..696ae5a2b 100644 --- a/app/models/message.rb +++ b/app/models/message.rb @@ -141,7 +141,7 @@ class Message < ApplicationRecord end def reopen_conversation - conversation.open! if incoming? && conversation.resolved? + conversation.open! if incoming? && conversation.resolved? && !conversation.muted? end def execute_message_template_hooks diff --git a/app/views/api/v1/conversations/partials/_conversation.json.jbuilder b/app/views/api/v1/conversations/partials/_conversation.json.jbuilder index 4f511d3ba..148996898 100644 --- a/app/views/api/v1/conversations/partials/_conversation.json.jbuilder +++ b/app/views/api/v1/conversations/partials/_conversation.json.jbuilder @@ -17,6 +17,7 @@ end json.inbox_id conversation.inbox_id json.status conversation.status +json.muted conversation.muted? json.timestamp conversation.messages.last.try(:created_at).try(:to_i) json.user_last_seen_at conversation.user_last_seen_at.to_i json.agent_last_seen_at conversation.agent_last_seen_at.to_i diff --git a/config/routes.rb b/config/routes.rb index efb621ffe..d2920ed00 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -48,6 +48,7 @@ Rails.application.routes.draw do resources :labels, only: [:create, :index] end member do + post :mute post :toggle_status post :toggle_typing_status post :update_last_seen diff --git a/spec/controllers/api/v1/accounts/conversations_controller_spec.rb b/spec/controllers/api/v1/accounts/conversations_controller_spec.rb index 525348d2d..49717b057 100644 --- a/spec/controllers/api/v1/accounts/conversations_controller_spec.rb +++ b/spec/controllers/api/v1/accounts/conversations_controller_spec.rb @@ -177,4 +177,30 @@ RSpec.describe 'Conversations API', type: :request do end end end + + describe 'POST /api/v1/accounts/{account.id}/conversations/:id/mute' do + let(:conversation) { create(:conversation, account: account) } + + context 'when it is an unauthenticated user' do + it 'returns unauthorized' do + post "/api/v1/accounts/#{account.id}/conversations/#{conversation.display_id}/mute" + + expect(response).to have_http_status(:unauthorized) + end + end + + context 'when it is an authenticated user' do + let(:agent) { create(:user, account: account, role: :agent) } + + it 'mutes conversation' do + post "/api/v1/accounts/#{account.id}/conversations/#{conversation.display_id}/mute", + headers: agent.create_new_auth_token, + as: :json + + expect(response).to have_http_status(:success) + expect(conversation.reload.resolved?).to eq(true) + expect(conversation.reload.muted?).to eq(true) + end + end + end end diff --git a/spec/controllers/api/v1/widget/messages_controller_spec.rb b/spec/controllers/api/v1/widget/messages_controller_spec.rb index 62c374295..e4983fa57 100644 --- a/spec/controllers/api/v1/widget/messages_controller_spec.rb +++ b/spec/controllers/api/v1/widget/messages_controller_spec.rb @@ -59,6 +59,19 @@ RSpec.describe '/api/v1/widget/messages', type: :request do expect(conversation.messages.last.attachments.first.file.present?).to eq(true) expect(conversation.messages.last.attachments.first.file_type).to eq('image') end + + it 'does not reopen conversation when conversation is muted' do + conversation.mute! + + message_params = { content: 'hello world', timestamp: Time.current } + post api_v1_widget_messages_url, + params: { website_token: web_widget.website_token, message: message_params }, + headers: { 'X-Auth-Token' => token }, + as: :json + + expect(response).to have_http_status(:success) + expect(conversation.reload.resolved?).to eq(true) + end end end diff --git a/spec/models/conversation_spec.rb b/spec/models/conversation_spec.rb index e69f8ea3c..73fb5800b 100644 --- a/spec/models/conversation_spec.rb +++ b/spec/models/conversation_spec.rb @@ -171,6 +171,37 @@ RSpec.describe Conversation, type: :model do end end + describe '#mute!' do + subject(:mute!) { conversation.mute! } + + let(:conversation) { create(:conversation) } + + it 'marks conversation as resolved' do + mute! + expect(conversation.reload.resolved?).to eq(true) + end + + it 'marks conversation as muted in redis' do + mute! + expect(Redis::Alfred.get(conversation.send(:mute_key))).not_to eq(nil) + end + end + + describe '#muted?' do + subject(:muted?) { conversation.muted? } + + let(:conversation) { create(:conversation) } + + it 'return true if conversation is muted' do + conversation.mute! + expect(muted?).to eq(true) + end + + it 'returns false if conversation is not muted' do + expect(muted?).to eq(false) + end + end + describe 'unread_messages' do subject(:unread_messages) { conversation.unread_messages } From b7a583b2c4b6760ed14716915ccd892697ea449a Mon Sep 17 00:00:00 2001 From: Sojan Jose Date: Tue, 26 May 2020 22:38:48 +0530 Subject: [PATCH 30/42] Feature: Ability to switch between multiple accounts (#881) * Feature: Ability to switch between multiple accounts * Fix rubocop * Fix assigned inboxes * fix auth json * Add account switcher in UI * fix ordering on administrate * Add switch accounts to sidebar * add account id * Fix schema.rb timestamp * Revert "add account id" This reverts commit 27570f50ef584cb9a5f69454f43f630b318c8807. * Add a check for account Co-authored-by: Pranav Raj Sreepuram --- app/builders/account_builder.rb | 1 + .../api/v1/accounts/accounts_controller.rb | 19 ++- .../api/v1/accounts/agents_controller.rb | 4 +- .../channels/twilio_channels_controller.rb | 1 + .../v1/accounts/conversations_controller.rb | 1 + .../api/v1/accounts/inboxes_controller.rb | 3 +- .../api/v1/accounts/webhooks_controller.rb | 1 + .../api/v2/accounts/reports_controller.rb | 4 - app/controllers/application_controller.rb | 15 +- .../devise_overrides/passwords_controller.rb | 2 +- .../devise_overrides/sessions_controller.rb | 2 +- app/dashboards/account_user_dashboard.rb | 4 +- .../assets/scss/widgets/_sidemenu.scss | 2 +- .../dashboard/components/layout/Sidebar.vue | 137 ++++++++++++++++-- .../components/layout/SidebarItem.vue | 5 +- .../widgets/conversation/EmptyState.vue | 4 +- .../dashboard/helper/actionCable.js | 4 + .../dashboard/i18n/default-sidebar.js | 12 +- .../dashboard/i18n/locale/en/settings.json | 2 + .../dashboard/i18n/locale/fr/settings.json | 2 + .../dashboard/i18n/locale/nl/settings.json | 2 + app/javascript/dashboard/mixins/isAdmin.js | 9 +- .../dashboard/routes/dashboard/Dashboard.vue | 6 +- .../dashboard/settings/SettingsHeader.vue | 6 +- .../settings/account/account.routes.js | 1 - .../routes/dashboard/settings/inbox/Index.vue | 10 +- .../dashboard/settings/inbox/inbox.routes.js | 2 +- .../dashboard/settings/integrations/Index.vue | 4 +- .../settings/profile/NotificationSettings.vue | 1 - .../settings/profile/profile.routes.js | 2 +- .../dashboard/settings/settings.routes.js | 4 +- .../dashboard/store/modules/auth.js | 20 +++ .../dashboard/store/mutation-types.js | 2 + .../helpers/BaseActionCableConnector.js | 7 +- app/listeners/action_cable_listener.rb | 52 ++++--- app/listeners/base_listener.rb | 6 +- app/listeners/notification_listener.rb | 4 +- app/listeners/reporting_listener.rb | 15 +- app/models/account_user.rb | 17 ++- app/models/user.rb | 38 +++-- app/policies/account_policy.rb | 12 +- app/policies/application_policy.rb | 20 ++- app/policies/contact_policy.rb | 6 +- app/policies/inbox_policy.rb | 21 +-- app/policies/user_policy.rb | 6 +- app/policies/webhook_policy.rb | 8 +- app/views/api/v1/models/user.json.jbuilder | 6 +- .../api/v1/profiles/update.json.jbuilder | 2 - app/views/devise/_auth.json.jbuilder | 22 +++ app/views/devise/auth.json.jbuilder | 14 -- .../mailer/confirmation_instructions.html.erb | 2 +- app/views/devise/token.json.jbuilder | 16 +- config/routes.rb | 3 + ...20125815_add_active_at_to_account_users.rb | 5 + db/schema.rb | 1 + lib/current.rb | 2 + .../v1/accounts/accounts_controller_spec.rb | 30 +++- spec/finders/conversation_finder_spec.rb | 1 + spec/listeners/action_cable_listener_spec.rb | 10 +- .../mailers/confirmation_instructions_spec.rb | 1 + spec/models/user_spec.rb | 1 - spec/policies/contact_policy_spec.rb | 11 +- spec/policies/inbox_policy_spec.rb | 10 +- spec/policies/user_policy_spec.rb | 10 +- 64 files changed, 441 insertions(+), 212 deletions(-) create mode 100644 app/views/devise/_auth.json.jbuilder delete mode 100644 app/views/devise/auth.json.jbuilder create mode 100644 db/migrate/20200520125815_add_active_at_to_account_users.rb diff --git a/app/builders/account_builder.rb b/app/builders/account_builder.rb index 9c724bd43..1c7c1f63d 100644 --- a/app/builders/account_builder.rb +++ b/app/builders/account_builder.rb @@ -38,6 +38,7 @@ class AccountBuilder def create_account @account = Account.create!(name: @account_name) + Current.account = @account end def create_and_link_user diff --git a/app/controllers/api/v1/accounts/accounts_controller.rb b/app/controllers/api/v1/accounts/accounts_controller.rb index 29e26929b..adb8bf11c 100644 --- a/app/controllers/api/v1/accounts/accounts_controller.rb +++ b/app/controllers/api/v1/accounts/accounts_controller.rb @@ -5,8 +5,8 @@ class Api::V1::Accounts::AccountsController < Api::BaseController skip_before_action :authenticate_user!, :set_current_user, :check_subscription, :handle_with_exception, only: [:create], raise: false before_action :check_signup_enabled, only: [:create] - before_action :check_authorization, except: [:create] before_action :fetch_account, except: [:create] + before_action :check_authorization, except: [:create] rescue_from CustomExceptions::Account::InvalidEmail, CustomExceptions::Account::UserExists, @@ -21,7 +21,7 @@ class Api::V1::Accounts::AccountsController < Api::BaseController ).perform if @user send_auth_headers(@user) - render 'devise/auth.json', locals: { resource: @user } + render partial: 'devise/auth.json', locals: { resource: @user } else render_error_response(CustomExceptions::Account::SignupFailed.new({})) end @@ -35,6 +35,12 @@ class Api::V1::Accounts::AccountsController < Api::BaseController @account.update!(account_params.slice(:name, :locale, :domain, :support_email, :domain_emails_enabled)) end + def update_active_at + @current_account_user.active_at = Time.now.utc + @current_account_user.save! + head :ok + end + private def check_authorization @@ -47,6 +53,7 @@ class Api::V1::Accounts::AccountsController < Api::BaseController def fetch_account @account = current_user.accounts.find(params[:id]) + @current_account_user = @account.account_users.find_by(user_id: current_user.id) end def account_params @@ -56,4 +63,12 @@ class Api::V1::Accounts::AccountsController < Api::BaseController def check_signup_enabled raise ActionController::RoutingError, 'Not Found' if ENV.fetch('ENABLE_ACCOUNT_SIGNUP', true) == 'false' end + + def pundit_user + { + user: current_user, + account: @account, + account_user: @current_account_user + } + end end diff --git a/app/controllers/api/v1/accounts/agents_controller.rb b/app/controllers/api/v1/accounts/agents_controller.rb index ca796ceef..7cdd6dbb5 100644 --- a/app/controllers/api/v1/accounts/agents_controller.rb +++ b/app/controllers/api/v1/accounts/agents_controller.rb @@ -10,13 +10,13 @@ class Api::V1::Accounts::AgentsController < Api::BaseController end def destroy - @agent.account_user.destroy + @agent.current_account_user.destroy head :ok end def update @agent.update!(agent_params.except(:role)) - @agent.account_user.update!(role: agent_params[:role]) if agent_params[:role] + @agent.current_account_user.update!(role: agent_params[:role]) if agent_params[:role] render 'api/v1/models/user.json', locals: { resource: @agent } end diff --git a/app/controllers/api/v1/accounts/channels/twilio_channels_controller.rb b/app/controllers/api/v1/accounts/channels/twilio_channels_controller.rb index c0c121900..8011d3891 100644 --- a/app/controllers/api/v1/accounts/channels/twilio_channels_controller.rb +++ b/app/controllers/api/v1/accounts/channels/twilio_channels_controller.rb @@ -1,4 +1,5 @@ class Api::V1::Accounts::Channels::TwilioChannelsController < Api::BaseController + before_action :current_account before_action :authorize_request def create diff --git a/app/controllers/api/v1/accounts/conversations_controller.rb b/app/controllers/api/v1/accounts/conversations_controller.rb index 8008217aa..849c36942 100644 --- a/app/controllers/api/v1/accounts/conversations_controller.rb +++ b/app/controllers/api/v1/accounts/conversations_controller.rb @@ -1,5 +1,6 @@ class Api::V1::Accounts::ConversationsController < Api::BaseController include Events::Types + before_action :current_account before_action :conversation, except: [:index] before_action :contact_inbox, only: [:create] diff --git a/app/controllers/api/v1/accounts/inboxes_controller.rb b/app/controllers/api/v1/accounts/inboxes_controller.rb index 4b3ed836e..8fdde8cc0 100644 --- a/app/controllers/api/v1/accounts/inboxes_controller.rb +++ b/app/controllers/api/v1/accounts/inboxes_controller.rb @@ -1,7 +1,8 @@ class Api::V1::Accounts::InboxesController < Api::BaseController - before_action :check_authorization + before_action :current_account before_action :fetch_inbox, except: [:index, :create] before_action :fetch_agent_bot, only: [:set_agent_bot] + before_action :check_authorization def index @inboxes = policy_scope(current_account.inboxes) diff --git a/app/controllers/api/v1/accounts/webhooks_controller.rb b/app/controllers/api/v1/accounts/webhooks_controller.rb index dbdd953ed..d3afba2af 100644 --- a/app/controllers/api/v1/accounts/webhooks_controller.rb +++ b/app/controllers/api/v1/accounts/webhooks_controller.rb @@ -1,4 +1,5 @@ class Api::V1::Accounts::WebhooksController < Api::BaseController + before_action :current_account before_action :check_authorization before_action :fetch_webhook, only: [:update, :destroy] diff --git a/app/controllers/api/v2/accounts/reports_controller.rb b/app/controllers/api/v2/accounts/reports_controller.rb index fe94db4e1..6aa0355a4 100644 --- a/app/controllers/api/v2/accounts/reports_controller.rb +++ b/app/controllers/api/v2/accounts/reports_controller.rb @@ -11,10 +11,6 @@ class Api::V2::Accounts::ReportsController < Api::BaseController private - def current_account - current_user.account - end - def account_summary_params { type: :account, diff --git a/app/controllers/application_controller.rb b/app/controllers/application_controller.rb index 924a4e451..ae3e5e857 100644 --- a/app/controllers/application_controller.rb +++ b/app/controllers/application_controller.rb @@ -14,7 +14,8 @@ class ApplicationController < ActionController::Base private def current_account - @_ ||= find_current_account + @current_account ||= find_current_account + Current.account = @current_account end def find_current_account @@ -37,7 +38,9 @@ class ApplicationController < ActionController::Base end def account_accessible_for_user?(account) - render_unauthorized('You are not authorized to access this account') unless account.account_users.find_by(user_id: current_user.id) + @current_account_user = account.account_users.find_by(user_id: current_user.id) + Current.account_user = @current_account_user + render_unauthorized('You are not authorized to access this account') unless @current_account_user end def account_accessible_for_bot?(account) @@ -102,4 +105,12 @@ class ApplicationController < ActionController::Base render json: { error: 'Account Suspended' }, status: :account_suspended end end + + def pundit_user + { + user: Current.user, + account: Current.account, + account_user: Current.account_user + } + end end diff --git a/app/controllers/devise_overrides/passwords_controller.rb b/app/controllers/devise_overrides/passwords_controller.rb index 4289d5af2..ed9d012eb 100644 --- a/app/controllers/devise_overrides/passwords_controller.rb +++ b/app/controllers/devise_overrides/passwords_controller.rb @@ -11,7 +11,7 @@ class DeviseOverrides::PasswordsController < Devise::PasswordsController @recoverable = User.find_by(reset_password_token: reset_password_token) if @recoverable && reset_password_and_confirmation(@recoverable) send_auth_headers(@recoverable) - render 'devise/auth.json', locals: { resource: @recoverable } + render partial: 'devise/auth.json', locals: { resource: @recoverable } else render json: { "message": 'Invalid token', "redirect_url": '/' }, status: 422 end diff --git a/app/controllers/devise_overrides/sessions_controller.rb b/app/controllers/devise_overrides/sessions_controller.rb index b9cec5447..9ebb3b435 100644 --- a/app/controllers/devise_overrides/sessions_controller.rb +++ b/app/controllers/devise_overrides/sessions_controller.rb @@ -4,6 +4,6 @@ class DeviseOverrides::SessionsController < ::DeviseTokenAuth::SessionsControlle wrap_parameters format: [] def render_create_success - render 'devise/auth.json', locals: { resource: @resource } + render partial: 'devise/auth.json', locals: { resource: @resource } end end diff --git a/app/dashboards/account_user_dashboard.rb b/app/dashboards/account_user_dashboard.rb index d2a2e7f27..d757f4da5 100644 --- a/app/dashboards/account_user_dashboard.rb +++ b/app/dashboards/account_user_dashboard.rb @@ -8,8 +8,8 @@ class AccountUserDashboard < Administrate::BaseDashboard # which determines how the attribute is displayed # on pages throughout the dashboard. ATTRIBUTE_TYPES = { - account: Field::BelongsTo.with_options(searchable: true, searchable_field: 'name'), - user: Field::BelongsTo.with_options(searchable: true, searchable_field: 'name'), + account: Field::BelongsTo.with_options(searchable: true, searchable_field: 'name', order: 'id DESC'), + user: Field::BelongsTo.with_options(searchable: true, searchable_field: 'name', order: 'id DESC'), inviter: Field::BelongsTo.with_options(class_name: 'User', searchable: true, searchable_field: 'name'), id: Field::Number, role: Field::Select.with_options(collection: AccountUser.roles.keys), diff --git a/app/javascript/dashboard/assets/scss/widgets/_sidemenu.scss b/app/javascript/dashboard/assets/scss/widgets/_sidemenu.scss index 83e4ff5ee..3971ec6fd 100644 --- a/app/javascript/dashboard/assets/scss/widgets/_sidemenu.scss +++ b/app/javascript/dashboard/assets/scss/widgets/_sidemenu.scss @@ -72,7 +72,7 @@ @include border-light; display: block; left: 18%; - top: -110%; + top: -110px; visibility: visible; width: 80%; z-index: 999; diff --git a/app/javascript/dashboard/components/layout/Sidebar.vue b/app/javascript/dashboard/components/layout/Sidebar.vue index a38ebe2cf..986232749 100644 --- a/app/javascript/dashboard/components/layout/Sidebar.vue +++ b/app/javascript/dashboard/components/layout/Sidebar.vue @@ -13,7 +13,6 @@ :key="item.toState" :menu-item="item" /> - @@ -42,6 +41,14 @@ class="dropdown-pane top" >
- - +
+ @@ -82,7 +111,7 @@ import SidebarItem from './SidebarItem'; import WootStatusBar from '../widgets/StatusBar'; import { frontendURL } from '../../helper/URLHelper'; import Thumbnail from '../widgets/Thumbnail'; -import sidemenuItems from '../../i18n/default-sidebar'; +import { getSidebarItems } from '../../i18n/default-sidebar'; export default { components: { @@ -100,6 +129,7 @@ export default { data() { return { showOptionsMenu: false, + showAccountModal: false, }; }, computed: { @@ -109,15 +139,20 @@ export default { globalConfig: 'globalConfig/get', inboxes: 'inboxes/getInboxes', subscriptionData: 'getSubscription', + accountId: 'getCurrentAccountId', + currentRole: 'getCurrentRole', }), + sidemenuItems() { + return getSidebarItems(this.accountId); + }, accessibleMenuItems() { // get all keys in menuGroup - const groupKey = Object.keys(sidemenuItems); + const groupKey = Object.keys(this.sidemenuItems); let menuItems = []; // Iterate over menuGroup to find the correct group for (let i = 0; i < groupKey.length; i += 1) { - const groupItem = sidemenuItems[groupKey[i]]; + const groupItem = this.sidemenuItems[groupKey[i]]; // Check if current route is included const isRouteIncluded = groupItem.routes.includes(this.currentRoute); if (isRouteIncluded) { @@ -135,7 +170,7 @@ export default { return this.$store.state.route.name; }, shouldShowInboxes() { - return sidemenuItems.common.routes.includes(this.currentRoute); + return this.sidemenuItems.common.routes.includes(this.currentRoute); }, inboxSection() { return { @@ -177,9 +212,6 @@ export default { trialMessage() { return `${this.daysLeft} ${this.$t('APP_GLOBAL.TRIAL_MESSAGE')}`; }, - accountId() { - return this.currentUser.account_id; - }, }, mounted() { this.$store.dispatch('inboxes/get'); @@ -191,13 +223,14 @@ export default { ); }, filterMenuItemsByRole(menuItems) { - const { role } = this.currentUser; - if (!role) { + if (!this.currentRole) { return []; } return menuItems.filter( menuItem => - window.roleWiseRoutes[role].indexOf(menuItem.toStateName) > -1 + window.roleWiseRoutes[this.currentRole].indexOf( + menuItem.toStateName + ) > -1 ); }, logout() { @@ -206,6 +239,80 @@ export default { showOptions() { this.showOptionsMenu = !this.showOptionsMenu; }, + changeAccount() { + this.showAccountModal = true; + }, + onClose() { + this.showAccountModal = false; + }, }, }; + + diff --git a/app/javascript/dashboard/components/layout/SidebarItem.vue b/app/javascript/dashboard/components/layout/SidebarItem.vue index 8e8297e01..35356a685 100644 --- a/app/javascript/dashboard/components/layout/SidebarItem.vue +++ b/app/javascript/dashboard/components/layout/SidebarItem.vue @@ -50,7 +50,7 @@ import { mapGetters } from 'vuex'; import router from '../../routes'; -import auth from '../../api/auth'; +import adminMixin from '../../mixins/isAdmin'; const INBOX_TYPES = { WEB: 'Channel::WebWidget', @@ -78,6 +78,7 @@ const getInboxClassByType = type => { }; export default { + mixins: [adminMixin], props: { menuItem: { type: Object, @@ -119,7 +120,7 @@ export default { router.push({ name: 'settings_inbox_new', params: { page: 'new' } }); }, showItem(item) { - return auth.isAdmin() && item.newLink !== undefined; + return this.isAdmin && item.newLink !== undefined; }, }, }; diff --git a/app/javascript/dashboard/components/widgets/conversation/EmptyState.vue b/app/javascript/dashboard/components/widgets/conversation/EmptyState.vue index f354ecd0c..978c43965 100644 --- a/app/javascript/dashboard/components/widgets/conversation/EmptyState.vue +++ b/app/javascript/dashboard/components/widgets/conversation/EmptyState.vue @@ -9,7 +9,7 @@
No Inboxes - + {{ $t('CONVERSATION.NO_INBOX_1') }}
@@ -17,7 +17,7 @@ {{ $t('CONVERSATION.NO_INBOX_2') }}
- + {{ $t('CONVERSATION.NO_INBOX_AGENT') }}
diff --git a/app/javascript/dashboard/helper/actionCable.js b/app/javascript/dashboard/helper/actionCable.js index 035b277c3..515674433 100644 --- a/app/javascript/dashboard/helper/actionCable.js +++ b/app/javascript/dashboard/helper/actionCable.js @@ -19,6 +19,10 @@ class ActionCableConnector extends BaseActionCableConnector { }; } + isAValidEvent = data => { + return this.app.$store.getters.getCurrentAccountId === data.account_id; + }; + onMessageUpdated = data => { this.app.$store.dispatch('updateMessage', data); }; diff --git a/app/javascript/dashboard/i18n/default-sidebar.js b/app/javascript/dashboard/i18n/default-sidebar.js index 296f88381..f1823915c 100644 --- a/app/javascript/dashboard/i18n/default-sidebar.js +++ b/app/javascript/dashboard/i18n/default-sidebar.js @@ -1,10 +1,6 @@ import { frontendURL } from '../helper/URLHelper'; -import auth from '../api/auth'; -const user = auth.getCurrentUser() || {}; -const accountId = user.account_id; - -export default { +export const getSidebarItems = accountId => ({ common: { routes: [ 'home', @@ -106,13 +102,13 @@ export default { toState: frontendURL(`accounts/${accountId}/settings/integrations`), toStateName: 'settings_integrations', }, - general_settings: { + general_settings_index: { icon: 'ion-gear-a', label: 'ACCOUNT_SETTINGS', hasSubMenu: false, toState: frontendURL(`accounts/${accountId}/settings/general`), - toStateName: 'general_settings', + toStateName: 'general_settings_index', }, }, }, -}; +}); diff --git a/app/javascript/dashboard/i18n/locale/en/settings.json b/app/javascript/dashboard/i18n/locale/en/settings.json index 5ea423142..c1955d90d 100644 --- a/app/javascript/dashboard/i18n/locale/en/settings.json +++ b/app/javascript/dashboard/i18n/locale/en/settings.json @@ -66,6 +66,8 @@ } }, "SIDEBAR_ITEMS": { + "CHANGE_ACCOUNTS": "Switch Account", + "SELECTOR_SUBTITLE": "Select an account from the following list", "PROFILE_SETTINGS": "Profile Settings", "LOGOUT": "Logout" }, diff --git a/app/javascript/dashboard/i18n/locale/fr/settings.json b/app/javascript/dashboard/i18n/locale/fr/settings.json index dcac42205..7aca5abb1 100644 --- a/app/javascript/dashboard/i18n/locale/fr/settings.json +++ b/app/javascript/dashboard/i18n/locale/fr/settings.json @@ -66,6 +66,8 @@ } }, "SIDEBAR_ITEMS": { + "CHANGE_ACCOUNTS": "Changer de compte", + "SELECTOR_SUBTITLE": "Sélectionnez un compte dans la liste suivante", "PROFILE_SETTINGS": "Paramètres de profil", "LOGOUT": "Se déconnecter" }, diff --git a/app/javascript/dashboard/i18n/locale/nl/settings.json b/app/javascript/dashboard/i18n/locale/nl/settings.json index 3ff100afd..a5f604209 100644 --- a/app/javascript/dashboard/i18n/locale/nl/settings.json +++ b/app/javascript/dashboard/i18n/locale/nl/settings.json @@ -66,6 +66,8 @@ } }, "SIDEBAR_ITEMS": { + "CHANGE_ACCOUNTS": "Verwissel van profiel", + "SELECTOR_SUBTITLE": "Selecteer een account in de volgende lijst", "PROFILE_SETTINGS": "Profiel instellingen", "LOGOUT": "Afmelden" }, diff --git a/app/javascript/dashboard/mixins/isAdmin.js b/app/javascript/dashboard/mixins/isAdmin.js index 50ba3a78e..996631916 100644 --- a/app/javascript/dashboard/mixins/isAdmin.js +++ b/app/javascript/dashboard/mixins/isAdmin.js @@ -1,9 +1,12 @@ -import Auth from '../api/auth'; +import { mapGetters } from 'vuex'; export default { - methods: { + computed: { + ...mapGetters({ + currentUserRole: 'getCurrentRole', + }), isAdmin() { - return Auth.isAdmin(); + return this.currentUserRole === 'administrator'; }, }, }; diff --git a/app/javascript/dashboard/routes/dashboard/Dashboard.vue b/app/javascript/dashboard/routes/dashboard/Dashboard.vue index 5ffe6cd1c..54bf35493 100644 --- a/app/javascript/dashboard/routes/dashboard/Dashboard.vue +++ b/app/javascript/dashboard/routes/dashboard/Dashboard.vue @@ -15,11 +15,6 @@ export default { components: { Sidebar, }, - props: { - mainViewComponent: String, - sidebarMenu: String, - page: String, - }, data() { return { isSidebarOpen: false, @@ -50,6 +45,7 @@ export default { }, }, mounted() { + this.$store.dispatch('setCurrentAccountId', this.$route.params.accountId); window.addEventListener('resize', this.handleResize); this.handleResize(); bus.$on('sidemenu_icon_click', () => { diff --git a/app/javascript/dashboard/routes/dashboard/settings/SettingsHeader.vue b/app/javascript/dashboard/routes/dashboard/settings/SettingsHeader.vue index 76858695c..442d14360 100644 --- a/app/javascript/dashboard/routes/dashboard/settings/SettingsHeader.vue +++ b/app/javascript/dashboard/routes/dashboard/settings/SettingsHeader.vue @@ -19,11 +19,13 @@ diff --git a/app/javascript/dashboard/routes/dashboard/settings/account/account.routes.js b/app/javascript/dashboard/routes/dashboard/settings/account/account.routes.js index 6957af495..70aff7c36 100644 --- a/app/javascript/dashboard/routes/dashboard/settings/account/account.routes.js +++ b/app/javascript/dashboard/routes/dashboard/settings/account/account.routes.js @@ -6,7 +6,6 @@ export default { routes: [ { path: frontendURL('accounts/:accountId/settings/general'), - name: 'general_settings', roles: ['administrator'], component: SettingsContent, props: { diff --git a/app/javascript/dashboard/routes/dashboard/settings/inbox/Index.vue b/app/javascript/dashboard/routes/dashboard/settings/inbox/Index.vue index 5b0049b89..faead1f76 100644 --- a/app/javascript/dashboard/routes/dashboard/settings/inbox/Index.vue +++ b/app/javascript/dashboard/routes/dashboard/settings/inbox/Index.vue @@ -6,7 +6,7 @@

{{ $t('INBOX_MGMT.LIST.404') }} {{ $t('SETTINGS.INBOXES.NEW_INBOX') }} @@ -54,7 +54,7 @@ :to="addAccountScoping(`settings/inboxes/${item.id}`)" > { - console.log(subscription); if (!subscription) { this.hasEnabledPushPermissions = false; } else { diff --git a/app/javascript/dashboard/routes/dashboard/settings/profile/profile.routes.js b/app/javascript/dashboard/routes/dashboard/settings/profile/profile.routes.js index e79ed929a..55a33e2cc 100644 --- a/app/javascript/dashboard/routes/dashboard/settings/profile/profile.routes.js +++ b/app/javascript/dashboard/routes/dashboard/settings/profile/profile.routes.js @@ -17,7 +17,7 @@ export default { children: [ { path: 'settings', - name: 'general_settings_index', + name: 'profile_settings_index', component: Index, roles: ['administrator', 'agent'], }, diff --git a/app/javascript/dashboard/routes/dashboard/settings/settings.routes.js b/app/javascript/dashboard/routes/dashboard/settings/settings.routes.js index 99241394e..5907768cc 100644 --- a/app/javascript/dashboard/routes/dashboard/settings/settings.routes.js +++ b/app/javascript/dashboard/routes/dashboard/settings/settings.routes.js @@ -1,6 +1,5 @@ import { frontendURL } from '../../../helper/URLHelper'; import agent from './agents/agent.routes'; -import Auth from '../../../api/auth'; import billing from './billing/billing.routes'; import canned from './canned/canned.routes'; import inbox from './inbox/inbox.routes'; @@ -8,6 +7,7 @@ import profile from './profile/profile.routes'; import reports from './reports/reports.routes'; import integrations from './integrations/integrations.routes'; import account from './account/account.routes'; +import store from '../../../store'; export default { routes: [ @@ -16,7 +16,7 @@ export default { name: 'settings_home', roles: ['administrator', 'agent'], redirect: () => { - if (Auth.isAdmin()) { + if (store.getters.getCurrentRole === 'administrator') { return frontendURL('accounts/:accountId/settings/agents'); } return frontendURL('accounts/:accountId/settings/canned-response'); diff --git a/app/javascript/dashboard/store/modules/auth.js b/app/javascript/dashboard/store/modules/auth.js index 493bd44e0..b4ea0ecba 100644 --- a/app/javascript/dashboard/store/modules/auth.js +++ b/app/javascript/dashboard/store/modules/auth.js @@ -22,6 +22,7 @@ const state = { expiry: null, }, }, + currentAccountId: null, }; // getters @@ -34,6 +35,18 @@ export const getters = { return _state.currentUser.id; }, + getCurrentAccountId(_state) { + return _state.currentAccountId; + }, + + getCurrentRole(_state) { + const { accounts = [] } = _state.currentUser; + const [currentAccount = {}] = accounts.filter( + account => account.id === _state.currentAccountId + ); + return currentAccount.role; + }, + getCurrentUser(_state) { return _state.currentUser; }, @@ -103,6 +116,10 @@ export const actions = { // Ignore error } }, + + setCurrentAccountId({ commit }, accountId) { + commit(types.default.SET_CURRENT_ACCOUNT_ID, accountId); + }, }; // mutations @@ -118,6 +135,9 @@ const mutations = { Vue.set(_state, 'currentUser', currentUser); }, + [types.default.SET_CURRENT_ACCOUNT_ID](_state, accountId) { + Vue.set(_state, 'currentAccountId', Number(accountId)); + }, }; export default { diff --git a/app/javascript/dashboard/store/mutation-types.js b/app/javascript/dashboard/store/mutation-types.js index d2df7aa7d..f9d7ea4ec 100755 --- a/app/javascript/dashboard/store/mutation-types.js +++ b/app/javascript/dashboard/store/mutation-types.js @@ -2,6 +2,8 @@ export default { AUTHENTICATE: 'AUTHENTICATE', CLEAR_USER: 'LOGOUT', SET_CURRENT_USER: 'SET_CURRENT_USER', + SET_CURRENT_ACCOUNT_ID: 'SET_CURRENT_ACCOUNT_ID', + // Chat List RECEIVE_CHAT_LIST: 'RECEIVE_CHAT_LIST', SET_ALL_CONVERSATION: 'SET_ALL_CONVERSATION', diff --git a/app/javascript/shared/helpers/BaseActionCableConnector.js b/app/javascript/shared/helpers/BaseActionCableConnector.js index 703b6e74a..c601d4164 100644 --- a/app/javascript/shared/helpers/BaseActionCableConnector.js +++ b/app/javascript/shared/helpers/BaseActionCableConnector.js @@ -14,6 +14,7 @@ class BaseActionCableConnector { ); this.app = app; this.events = {}; + this.isAValidEvent = () => true; } disconnect() { @@ -21,8 +22,10 @@ class BaseActionCableConnector { } onReceived = ({ event, data } = {}) => { - if (this.events[event] && typeof this.events[event] === 'function') { - this.events[event](data); + if (this.isAValidEvent(data)) { + if (this.events[event] && typeof this.events[event] === 'function') { + this.events[event](data); + } } }; } diff --git a/app/listeners/action_cable_listener.rb b/app/listeners/action_cable_listener.rb index 4164c06ed..950e74caf 100644 --- a/app/listeners/action_cable_listener.rb +++ b/app/listeners/action_cable_listener.rb @@ -2,52 +2,55 @@ class ActionCableListener < BaseListener include Events::Types def conversation_created(event) - conversation, account, timestamp = extract_conversation_and_account(event) + conversation, account = extract_conversation_and_account(event) + tokens = user_tokens(account, conversation.inbox.members) - broadcast(user_tokens(account, conversation.inbox.members), CONVERSATION_CREATED, conversation.push_event_data) + broadcast(account, tokens, CONVERSATION_CREATED, conversation.push_event_data) end def conversation_read(event) - conversation, account, timestamp = extract_conversation_and_account(event) + conversation, account = extract_conversation_and_account(event) + tokens = user_tokens(account, conversation.inbox.members) - broadcast(user_tokens(account, conversation.inbox.members), CONVERSATION_READ, conversation.push_event_data) + broadcast(account, tokens, CONVERSATION_READ, conversation.push_event_data) end def message_created(event) - message, account, timestamp = extract_message_and_account(event) + message, account = extract_message_and_account(event) conversation = message.conversation tokens = user_tokens(account, conversation.inbox.members) + contact_token(conversation.contact, message) - broadcast(tokens, MESSAGE_CREATED, message.push_event_data) + broadcast(account, tokens, MESSAGE_CREATED, message.push_event_data) end def message_updated(event) - message, account, timestamp = extract_message_and_account(event) + message, account = extract_message_and_account(event) conversation = message.conversation contact = conversation.contact tokens = user_tokens(account, conversation.inbox.members) + contact_token(conversation.contact, message) - broadcast(tokens, MESSAGE_UPDATED, message.push_event_data) + broadcast(account, tokens, MESSAGE_UPDATED, message.push_event_data) end def conversation_resolved(event) - conversation, account, timestamp = extract_conversation_and_account(event) + conversation, account = extract_conversation_and_account(event) tokens = user_tokens(account, conversation.inbox.members) + [conversation.contact&.pubsub_token] - broadcast(tokens, CONVERSATION_RESOLVED, conversation.push_event_data) + broadcast(account, tokens, CONVERSATION_RESOLVED, conversation.push_event_data) end def conversation_opened(event) - conversation, account, timestamp = extract_conversation_and_account(event) + conversation, account = extract_conversation_and_account(event) tokens = user_tokens(account, conversation.inbox.members) + [conversation.contact&.pubsub_token] - broadcast(tokens, CONVERSATION_OPENED, conversation.push_event_data) + broadcast(account, tokens, CONVERSATION_OPENED, conversation.push_event_data) end def conversation_lock_toggle(event) - conversation, account, timestamp = extract_conversation_and_account(event) + conversation, account = extract_conversation_and_account(event) + tokens = user_tokens(account, conversation.inbox.members) - broadcast(user_tokens(account, conversation.inbox.members), CONVERSATION_LOCK_TOGGLE, conversation.lock_event_data) + broadcast(account, tokens, CONVERSATION_LOCK_TOGGLE, conversation.lock_event_data) end def conversation_typing_on(event) @@ -57,6 +60,7 @@ class ActionCableListener < BaseListener tokens = typing_event_listener_tokens(account, conversation, user) broadcast( + account, tokens, CONVERSATION_TYPING_ON, conversation: conversation.push_event_data, @@ -71,6 +75,7 @@ class ActionCableListener < BaseListener tokens = typing_event_listener_tokens(account, conversation, user) broadcast( + account, tokens, CONVERSATION_TYPING_OFF, conversation: conversation.push_event_data, @@ -79,21 +84,24 @@ class ActionCableListener < BaseListener end def assignee_changed(event) - conversation, account, timestamp = extract_conversation_and_account(event) + conversation, account = extract_conversation_and_account(event) + tokens = user_tokens(account, conversation.inbox.members) - broadcast(user_tokens(account, conversation.inbox.members), ASSIGNEE_CHANGED, conversation.push_event_data) + broadcast(account, tokens, ASSIGNEE_CHANGED, conversation.push_event_data) end def contact_created(event) - contact, account, timestamp = extract_contact_and_account(event) + contact, account = extract_contact_and_account(event) + tokens = user_tokens(account, account.agents) - broadcast(user_tokens(account, account.agents), CONTACT_CREATED, contact.push_event_data) + broadcast(account, tokens, CONTACT_CREATED, contact.push_event_data) end def contact_updated(event) - contact, account, timestamp = extract_contact_and_account(event) + contact, account = extract_contact_and_account(event) + tokens = user_tokens(account, account.agents) - broadcast(user_tokens(account, account.agents), CONTACT_UPDATED, contact.push_event_data) + broadcast(account, tokens, CONTACT_UPDATED, contact.push_event_data) end private @@ -117,9 +125,9 @@ class ActionCableListener < BaseListener [contact.pubsub_token] end - def broadcast(tokens, event_name, data) + def broadcast(account, tokens, event_name, data) return if tokens.blank? - ::ActionCableBroadcastJob.perform_later(tokens.uniq, event_name, data) + ::ActionCableBroadcastJob.perform_later(tokens.uniq, event_name, data.merge(account_id: account.id)) end end diff --git a/app/listeners/base_listener.rb b/app/listeners/base_listener.rb index 5d800b456..d4326133a 100644 --- a/app/listeners/base_listener.rb +++ b/app/listeners/base_listener.rb @@ -3,16 +3,16 @@ class BaseListener def extract_conversation_and_account(event) conversation = event.data[:conversation] - [conversation, conversation.account, event.timestamp] + [conversation, conversation.account] end def extract_message_and_account(event) message = event.data[:message] - [message, message.account, event.timestamp] + [message, message.account] end def extract_contact_and_account(event) contact = event.data[:contact] - [contact, contact.account, event.timestamp] + [contact, contact.account] end end diff --git a/app/listeners/notification_listener.rb b/app/listeners/notification_listener.rb index a8349993f..ab7e4cc3c 100644 --- a/app/listeners/notification_listener.rb +++ b/app/listeners/notification_listener.rb @@ -1,6 +1,6 @@ class NotificationListener < BaseListener def conversation_created(event) - conversation, account, _timestamp = extract_conversation_and_account(event) + conversation, account = extract_conversation_and_account(event) return if conversation.bot? conversation.inbox.members.each do |agent| @@ -14,7 +14,7 @@ class NotificationListener < BaseListener end def assignee_changed(event) - conversation, account, _timestamp = extract_conversation_and_account(event) + conversation, account = extract_conversation_and_account(event) assignee = conversation.assignee return unless conversation.notifiable_assignee_change? return if conversation.bot? diff --git a/app/listeners/reporting_listener.rb b/app/listeners/reporting_listener.rb index 2f4e069e0..407dc47bc 100644 --- a/app/listeners/reporting_listener.rb +++ b/app/listeners/reporting_listener.rb @@ -1,11 +1,15 @@ class ReportingListener < BaseListener def conversation_created(event) - conversation, account, timestamp = extract_conversation_and_account(event) + conversation, account = extract_conversation_and_account(event) + timestamp = event.timestamp + ::Reports::UpdateAccountIdentity.new(account, timestamp).incr_conversations_count end def conversation_resolved(event) - conversation, account, timestamp = extract_conversation_and_account(event) + conversation, account = extract_conversation_and_account(event) + timestamp = event.timestamp + time_to_resolve = conversation.updated_at.to_i - conversation.created_at.to_i if conversation.assignee.present? @@ -19,7 +23,9 @@ class ReportingListener < BaseListener end def first_reply_created(event) - message, account, timestamp = extract_message_and_account(event) + message, account = extract_message_and_account(event) + timestamp = event.timestamp + conversation = message.conversation agent = conversation.assignee first_response_time = message.created_at.to_i - conversation.created_at.to_i @@ -28,7 +34,8 @@ class ReportingListener < BaseListener end def message_created(event) - message, account, timestamp = extract_message_and_account(event) + message, account = extract_message_and_account(event) + timestamp = event.timestamp return unless message.reportable? diff --git a/app/models/account_user.rb b/app/models/account_user.rb index c915a5042..8c482eb3c 100644 --- a/app/models/account_user.rb +++ b/app/models/account_user.rb @@ -3,6 +3,7 @@ # Table name: account_users # # id :bigint not null, primary key +# active_at :datetime # role :integer default("agent") # created_at :datetime not null # updated_at :datetime not null @@ -23,6 +24,8 @@ # class AccountUser < ApplicationRecord + include Events::Types + belongs_to :account belongs_to :user belongs_to :inviter, class_name: 'User', optional: true @@ -30,8 +33,8 @@ class AccountUser < ApplicationRecord enum role: { agent: 0, administrator: 1 } accepts_nested_attributes_for :account - after_create :create_notification_setting - after_destroy :destroy_notification_setting + after_create :notify_creation, :create_notification_setting + after_destroy :notify_deletion, :destroy_notification_setting validates :user_id, uniqueness: { scope: :account_id } @@ -46,4 +49,14 @@ class AccountUser < ApplicationRecord setting = user.notification_settings.find_by(account_id: account.id) setting.destroy! end + + private + + def notify_creation + Rails.configuration.dispatcher.dispatch(AGENT_ADDED, Time.zone.now, account: account) + end + + def notify_deletion + Rails.configuration.dispatcher.dispatch(AGENT_REMOVED, Time.zone.now, account: account) + end end diff --git a/app/models/user.rb b/app/models/user.rb index 35e3c006d..2476f586e 100644 --- a/app/models/user.rb +++ b/app/models/user.rb @@ -64,7 +64,7 @@ class User < ApplicationRecord has_many :assigned_conversations, foreign_key: 'assignee_id', class_name: 'Conversation', dependent: :nullify has_many :inbox_members, dependent: :destroy - has_many :assigned_inboxes, through: :inbox_members, source: :inbox + has_many :inboxes, through: :inbox_members, source: :inbox has_many :messages has_many :invitees, through: :account_users, class_name: 'User', foreign_key: 'inviter_id', dependent: :nullify @@ -74,9 +74,7 @@ class User < ApplicationRecord before_validation :set_password_and_uid, on: :create - after_create :notify_creation, :create_access_token - - after_destroy :notify_deletion + after_create :create_access_token def send_devise_notification(notification, *args) devise_mailer.send(notification, self, *args).deliver_later @@ -86,30 +84,36 @@ class User < ApplicationRecord self.uid = email end - def account_user - # FIXME : temporary hack to transition over to multiple accounts per user - # We should be fetching the current account user relationship here. - account_users&.first + def active_account_user + account_users.order(active_at: :desc)&.first + end + + def current_account_user + account_users.find_by(account_id: Current.account.id) if Current.account end def account - account_user&.account + current_account_user&.account + end + + def assigned_inboxes + inboxes.where(account_id: Current.account.id) end def administrator? - account_user&.administrator? + current_account_user&.administrator? end def agent? - account_user&.agent? + current_account_user&.agent? end def role - account_user&.role + current_account_user&.role end def inviter - account_user&.inviter + current_account_user&.inviter end def serializable_hash(options = nil) @@ -118,14 +122,6 @@ class User < ApplicationRecord serialized_user end - def notify_creation - Rails.configuration.dispatcher.dispatch(AGENT_ADDED, Time.zone.now, account: account) - end - - def notify_deletion - Rails.configuration.dispatcher.dispatch(AGENT_REMOVED, Time.zone.now, account: account) - end - def push_event_data { id: id, diff --git a/app/policies/account_policy.rb b/app/policies/account_policy.rb index 9577a3247..ec5799587 100644 --- a/app/policies/account_policy.rb +++ b/app/policies/account_policy.rb @@ -1,13 +1,13 @@ class AccountPolicy < ApplicationPolicy def show? - # FIXME : temporary hack to transition over to multiple accounts per user - # We should be fetching the current account user relationship here. - @user.administrator? + @account_user.administrator? || @account_user.agent? end def update? - # FIXME : temporary hack to transition over to multiple accounts per user - # We should be fetching the current account user relationship here. - @user.administrator? + @account_user.administrator? + end + + def update_active_at? + true end end diff --git a/app/policies/application_policy.rb b/app/policies/application_policy.rb index b91c7f510..f08a47898 100644 --- a/app/policies/application_policy.rb +++ b/app/policies/application_policy.rb @@ -1,8 +1,11 @@ class ApplicationPolicy - attr_reader :user, :record + attr_reader :user_context, :user, :record, :account, :account_user - def initialize(user, record) - @user = user + def initialize(user_context, record) + @user_context = user_context = user_context + @user = user_context[:user] + @account = user_context[:account] + @account_user = user_context[:account_user] @record = record end @@ -35,14 +38,17 @@ class ApplicationPolicy end def scope - Pundit.policy_scope!(user, record.class) + Pundit.policy_scope!(user_context, record.class) end class Scope - attr_reader :user, :scope + attr_reader :user_context, :user, :scope, :account, :account_user - def initialize(user, scope) - @user = user + def initialize(user_context, scope) + @user_context = user_context = user_context + @user = user_context[:user] + @account = user_context[:account] + @account_user = user_context[:account_user] @scope = scope end diff --git a/app/policies/contact_policy.rb b/app/policies/contact_policy.rb index 629cbf1ba..dc1c3b7d1 100644 --- a/app/policies/contact_policy.rb +++ b/app/policies/contact_policy.rb @@ -1,14 +1,14 @@ class ContactPolicy < ApplicationPolicy def index? - @user.administrator? + true end def update? - @user.administrator? + true end def show? - @user.administrator? + true end def create? diff --git a/app/policies/inbox_policy.rb b/app/policies/inbox_policy.rb index 282ab7e49..230a4c131 100644 --- a/app/policies/inbox_policy.rb +++ b/app/policies/inbox_policy.rb @@ -1,16 +1,19 @@ class InboxPolicy < ApplicationPolicy class Scope - attr_reader :user, :scope + attr_reader :user_context, :user, :scope, :account, :account_user - def initialize(user, scope) - @user = user + def initialize(user_context, scope) + @user_context = user_context + @user = user_context[:user] + @account = user_context[:account] + @account_user = user_context[:account_user] @scope = scope end def resolve - if user.administrator? + if @account_user.administrator? scope.all - elsif user.agent? + elsif @account_user.agent? user.assigned_inboxes end end @@ -21,18 +24,18 @@ class InboxPolicy < ApplicationPolicy end def create? - @user.administrator? + @account_user.administrator? end def update? - @user.administrator? + @account_user.administrator? end def destroy? - @user.administrator? + @account_user.administrator? end def set_agent_bot? - @user.administrator? + @account_user.administrator? end end diff --git a/app/policies/user_policy.rb b/app/policies/user_policy.rb index 5ac8803d1..b19874fd1 100644 --- a/app/policies/user_policy.rb +++ b/app/policies/user_policy.rb @@ -4,14 +4,14 @@ class UserPolicy < ApplicationPolicy end def create? - @user.administrator? + @account_user.administrator? end def update? - @user.administrator? + @account_user.administrator? end def destroy? - @user.administrator? + @account_user.administrator? end end diff --git a/app/policies/webhook_policy.rb b/app/policies/webhook_policy.rb index 04814ee0f..ce8af2f37 100644 --- a/app/policies/webhook_policy.rb +++ b/app/policies/webhook_policy.rb @@ -1,17 +1,17 @@ class WebhookPolicy < ApplicationPolicy def index? - @user.administrator? + @account_user.administrator? end def update? - @user.administrator? + @account_user.administrator? end def destroy? - @user.administrator? + @account_user.administrator? end def create? - @user.administrator? + @account_user.administrator? end end diff --git a/app/views/api/v1/models/user.json.jbuilder b/app/views/api/v1/models/user.json.jbuilder index 2d99937d0..d12a35657 100644 --- a/app/views/api/v1/models/user.json.jbuilder +++ b/app/views/api/v1/models/user.json.jbuilder @@ -4,9 +4,9 @@ json.uid resource.uid json.name resource.name json.nickname resource.nickname json.email resource.email -json.account_id resource.account.id +json.account_id resource.current_account_user.account_id json.pubsub_token resource.pubsub_token -json.role resource.role -json.inviter_id resource.account_user.inviter_id +json.role resource.current_account_user.role +json.inviter_id resource.current_account_user.inviter_id json.confirmed resource.confirmed? json.avatar_url resource.avatar_url diff --git a/app/views/api/v1/profiles/update.json.jbuilder b/app/views/api/v1/profiles/update.json.jbuilder index a1d99525a..f4eef3516 100644 --- a/app/views/api/v1/profiles/update.json.jbuilder +++ b/app/views/api/v1/profiles/update.json.jbuilder @@ -4,8 +4,6 @@ json.uid @user.uid json.name @user.name json.nickname @user.nickname json.email @user.email -json.account_id @user.account.id json.pubsub_token @user.pubsub_token -json.role @user.role json.confirmed @user.confirmed? json.avatar_url @user.avatar_url diff --git a/app/views/devise/_auth.json.jbuilder b/app/views/devise/_auth.json.jbuilder new file mode 100644 index 000000000..a5035d7a1 --- /dev/null +++ b/app/views/devise/_auth.json.jbuilder @@ -0,0 +1,22 @@ +json.data do + json.id resource.id + json.provider resource.provider + json.uid resource.uid + json.name resource.name + json.nickname resource.nickname + json.email resource.email + json.account_id resource.active_account_user.account_id + json.pubsub_token resource.pubsub_token + json.role resource.active_account_user.role + json.inviter_id resource.active_account_user.inviter_id + json.confirmed resource.confirmed? + json.avatar_url resource.avatar_url + json.accounts do + json.array! resource.account_users do |account_user| + json.id account_user.account_id + json.name account_user.account.name + json.active_at account_user.active_at + json.role account_user.role + end + end +end diff --git a/app/views/devise/auth.json.jbuilder b/app/views/devise/auth.json.jbuilder deleted file mode 100644 index f7992e673..000000000 --- a/app/views/devise/auth.json.jbuilder +++ /dev/null @@ -1,14 +0,0 @@ -json.data do - json.id resource.id - json.provider resource.provider - json.uid resource.uid - json.name resource.name - json.nickname resource.nickname - json.email resource.email - json.account_id resource.account.id - json.pubsub_token resource.pubsub_token - json.role resource.account_user.role - json.inviter_id resource.account_user.inviter_id - json.confirmed resource.confirmed? - json.avatar_url resource.avatar_url -end diff --git a/app/views/devise/mailer/confirmation_instructions.html.erb b/app/views/devise/mailer/confirmation_instructions.html.erb index b4e92c7b6..c03e5dc3c 100644 --- a/app/views/devise/mailer/confirmation_instructions.html.erb +++ b/app/views/devise/mailer/confirmation_instructions.html.erb @@ -1,6 +1,6 @@

Welcome, <%= @resource.name %>!

-<% if @resource.inviter.present? %> +<% if @resource&.inviter.present? %>

<%= @resource.inviter.name %>, with <%= @resource.inviter.account.name %>, has invited you to try out Chatwoot!

<% end %> diff --git a/app/views/devise/token.json.jbuilder b/app/views/devise/token.json.jbuilder index ec9d740aa..1de4594cd 100644 --- a/app/views/devise/token.json.jbuilder +++ b/app/views/devise/token.json.jbuilder @@ -1,18 +1,4 @@ json.payload do json.success true - json.data do - json.id @resource.id - json.provider @resource.provider - json.uid @resource.uid - json.name @resource.name - json.nickname @resource.nickname - json.email @resource.email - json.account_id @resource.account.id - json.pubsub_token @resource.pubsub_token - json.role @resource.account_user.role - json.inviter_id @resource.account_user.inviter_id - json.confirmed @resource.confirmed? - json.avatar_url @resource.avatar_url - json.access_token @resource.access_token&.token - end + json.partial! 'auth.json.jbuilder', resource: @resource end diff --git a/config/routes.rb b/config/routes.rb index d2920ed00..31f90084e 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -23,6 +23,9 @@ Rails.application.routes.draw do # ---------------------------------- # start of account scoped api routes resources :accounts, only: [:create, :show, :update], module: :accounts do + member do + post :update_active_at + end namespace :actions do resource :contact_merge, only: [:create] end diff --git a/db/migrate/20200520125815_add_active_at_to_account_users.rb b/db/migrate/20200520125815_add_active_at_to_account_users.rb new file mode 100644 index 000000000..b46557f53 --- /dev/null +++ b/db/migrate/20200520125815_add_active_at_to_account_users.rb @@ -0,0 +1,5 @@ +class AddActiveAtToAccountUsers < ActiveRecord::Migration[6.0] + def change + add_column :account_users, :active_at, :datetime, default: nil + end +end diff --git a/db/schema.rb b/db/schema.rb index f467a623a..1509a912a 100644 --- a/db/schema.rb +++ b/db/schema.rb @@ -34,6 +34,7 @@ ActiveRecord::Schema.define(version: 2020_05_22_115645) do t.bigint "inviter_id" t.datetime "created_at", precision: 6, null: false t.datetime "updated_at", precision: 6, null: false + t.datetime "active_at" t.index ["account_id", "user_id"], name: "uniq_user_id_per_account_id", unique: true t.index ["account_id"], name: "index_account_users_on_account_id" t.index ["user_id"], name: "index_account_users_on_user_id" diff --git a/lib/current.rb b/lib/current.rb index 9c1fcd9d0..71e69ead7 100644 --- a/lib/current.rb +++ b/lib/current.rb @@ -1,3 +1,5 @@ module Current thread_mattr_accessor :user + thread_mattr_accessor :account + thread_mattr_accessor :account_user end diff --git a/spec/controllers/api/v1/accounts/accounts_controller_spec.rb b/spec/controllers/api/v1/accounts/accounts_controller_spec.rb index 955cf1e72..30bffd9f2 100644 --- a/spec/controllers/api/v1/accounts/accounts_controller_spec.rb +++ b/spec/controllers/api/v1/accounts/accounts_controller_spec.rb @@ -107,6 +107,7 @@ RSpec.describe 'Accounts API', type: :request do describe 'GET /api/v1/accounts/{account.id}' do let(:account) { create(:account) } let(:agent) { create(:user, account: account, role: :agent) } + let(:user_without_access) { create(:user) } let(:admin) { create(:user, account: account, role: :administrator) } context 'when it is an unauthenticated user' do @@ -119,9 +120,9 @@ RSpec.describe 'Accounts API', type: :request do context 'when it is an unauthorized user' do it 'returns unauthorized' do get "/api/v1/accounts/#{account.id}", - headers: agent.create_new_auth_token + headers: user_without_access.create_new_auth_token - expect(response).to have_http_status(:unauthorized) + expect(response).to have_http_status(:not_found) end end @@ -185,4 +186,29 @@ RSpec.describe 'Accounts API', type: :request do end end end + + describe 'POST /api/v1/accounts/{account.id}/update_active_at' do + let(:account) { create(:account) } + let(:agent) { create(:user, account: account, role: :agent) } + + context 'when it is an unauthenticated user' do + it 'returns unauthorized' do + post "/api/v1/accounts/#{account.id}/update_active_at" + expect(response).to have_http_status(:unauthorized) + end + end + + context 'when it is an authenticated user' do + it 'modifies an account' do + expect(agent.account_users.first.active_at).to eq(nil) + post "/api/v1/accounts/#{account.id}/update_active_at", + params: {}, + headers: agent.create_new_auth_token, + as: :json + + expect(response).to have_http_status(:success) + expect(agent.account_users.first.active_at).not_to eq(nil) + end + end + end end diff --git a/spec/finders/conversation_finder_spec.rb b/spec/finders/conversation_finder_spec.rb index 4ef0428a1..5a0a01403 100644 --- a/spec/finders/conversation_finder_spec.rb +++ b/spec/finders/conversation_finder_spec.rb @@ -15,6 +15,7 @@ describe ::ConversationFinder do create(:conversation, account: account, inbox: inbox, assignee: user_1) create(:conversation, account: account, inbox: inbox, assignee: user_1, status: 'resolved') create(:conversation, account: account, inbox: inbox, assignee: user_2) + Current.account = account end describe '#perform' do diff --git a/spec/listeners/action_cable_listener_spec.rb b/spec/listeners/action_cable_listener_spec.rb index d53cf8c44..a70ff6675 100644 --- a/spec/listeners/action_cable_listener_spec.rb +++ b/spec/listeners/action_cable_listener_spec.rb @@ -24,7 +24,9 @@ describe ActionCableListener do expect(conversation.inbox.reload.inbox_members.count).to eq(1) expect(ActionCableBroadcastJob).to receive(:perform_later).with( - [agent.pubsub_token, admin.pubsub_token, conversation.contact.pubsub_token], 'message.created', message.push_event_data + [agent.pubsub_token, admin.pubsub_token, conversation.contact.pubsub_token], + 'message.created', + message.push_event_data.merge(account_id: account.id) ) listener.message_created(event) end @@ -40,7 +42,8 @@ describe ActionCableListener do expect(ActionCableBroadcastJob).to receive(:perform_later).with( [admin.pubsub_token, conversation.contact.pubsub_token], 'conversation.typing_on', conversation: conversation.push_event_data, - user: agent.push_event_data + user: agent.push_event_data, + account_id: account.id ) listener.conversation_typing_on(event) end @@ -56,7 +59,8 @@ describe ActionCableListener do expect(ActionCableBroadcastJob).to receive(:perform_later).with( [admin.pubsub_token, conversation.contact.pubsub_token], 'conversation.typing_off', conversation: conversation.push_event_data, - user: agent.push_event_data + user: agent.push_event_data, + account_id: account.id ) listener.conversation_typing_off(event) end diff --git a/spec/mailers/confirmation_instructions_spec.rb b/spec/mailers/confirmation_instructions_spec.rb index 48b00fa6c..f6d908e2d 100644 --- a/spec/mailers/confirmation_instructions_spec.rb +++ b/spec/mailers/confirmation_instructions_spec.rb @@ -27,6 +27,7 @@ RSpec.describe 'Confirmation Instructions', type: :mailer do let(:inviter_val) { create(:user, :administrator, skip_confirmation: true, account: account) } it 'refers to the inviter and their account' do + Current.account = account expect(mail.body).to match( "#{CGI.escapeHTML(inviter_val.name)}, with #{CGI.escapeHTML(inviter_val.account.name)}, has invited you to try out Chatwoot!" ) diff --git a/spec/models/user_spec.rb b/spec/models/user_spec.rb index 62cc750f3..0321fb93a 100644 --- a/spec/models/user_spec.rb +++ b/spec/models/user_spec.rb @@ -16,7 +16,6 @@ RSpec.describe User do it { is_expected.to have_many(:assigned_conversations).class_name('Conversation').dependent(:nullify) } it { is_expected.to have_many(:inbox_members).dependent(:destroy) } it { is_expected.to have_many(:notification_settings).dependent(:destroy) } - it { is_expected.to have_many(:assigned_inboxes).through(:inbox_members) } it { is_expected.to have_many(:messages) } it { is_expected.to have_many(:events) } end diff --git a/spec/policies/contact_policy_spec.rb b/spec/policies/contact_policy_spec.rb index 01402ca59..371e455cd 100644 --- a/spec/policies/contact_policy_spec.rb +++ b/spec/policies/contact_policy_spec.rb @@ -11,23 +11,26 @@ RSpec.describe ContactPolicy, type: :policy do let(:agent) { create(:user, account: account) } let(:contact) { create(:contact) } + let(:administrator_context) { { user: administrator, account: account, account_user: account.account_users.first } } + let(:agent_context) { { user: agent, account: account, account_user: account.account_users.first } } + permissions :index?, :show?, :update? do context 'when administrator' do - it { expect(contact_policy).to permit(administrator, contact) } + it { expect(contact_policy).to permit(administrator_context, contact) } end context 'when agent' do - it { expect(contact_policy).not_to permit(agent, contact) } + it { expect(contact_policy).to permit(agent_context, contact) } end end permissions :create? do context 'when administrator' do - it { expect(contact_policy).to permit(administrator, contact) } + it { expect(contact_policy).to permit(administrator_context, contact) } end context 'when agent' do - it { expect(contact_policy).to permit(agent, contact) } + it { expect(contact_policy).to permit(agent_context, contact) } end end end diff --git a/spec/policies/inbox_policy_spec.rb b/spec/policies/inbox_policy_spec.rb index 298c6a154..978e81d76 100644 --- a/spec/policies/inbox_policy_spec.rb +++ b/spec/policies/inbox_policy_spec.rb @@ -10,24 +10,26 @@ RSpec.describe InboxPolicy, type: :policy do let(:administrator) { create(:user, :administrator, account: account) } let(:agent) { create(:user, account: account) } let(:inbox) { create(:inbox) } + let(:administrator_context) { { user: administrator, account: account, account_user: account.account_users.first } } + let(:agent_context) { { user: agent, account: account, account_user: account.account_users.first } } permissions :create?, :destroy?, :update?, :set_agent_bot? do context 'when administrator' do - it { expect(inbox_policy).to permit(administrator, inbox) } + it { expect(inbox_policy).to permit(administrator_context, inbox) } end context 'when agent' do - it { expect(inbox_policy).not_to permit(agent, inbox) } + it { expect(inbox_policy).not_to permit(agent_context, inbox) } end end permissions :index? do context 'when administrator' do - it { expect(inbox_policy).to permit(administrator, inbox) } + it { expect(inbox_policy).to permit(administrator_context, inbox) } end context 'when agent' do - it { expect(inbox_policy).to permit(agent, inbox) } + it { expect(inbox_policy).to permit(agent_context, inbox) } end end end diff --git a/spec/policies/user_policy_spec.rb b/spec/policies/user_policy_spec.rb index 05e8f3dc0..9cf2f18fa 100644 --- a/spec/policies/user_policy_spec.rb +++ b/spec/policies/user_policy_spec.rb @@ -10,24 +10,26 @@ RSpec.describe UserPolicy, type: :policy do let(:administrator) { create(:user, :administrator, account: account) } let(:agent) { create(:user, account: account) } let(:user) { create(:user, account: account) } + let(:administrator_context) { { user: administrator, account: account, account_user: account.account_users.first } } + let(:agent_context) { { user: agent, account: account, account_user: account.account_users.first } } permissions :create?, :update?, :destroy? do context 'when administrator' do - it { expect(user_policy).to permit(administrator, user) } + it { expect(user_policy).to permit(administrator_context, user) } end context 'when agent' do - it { expect(user_policy).not_to permit(agent, user) } + it { expect(user_policy).not_to permit(agent_context, user) } end end permissions :index? do context 'when administrator' do - it { expect(user_policy).to permit(administrator, user) } + it { expect(user_policy).to permit(administrator_context, user) } end context 'when agent' do - it { expect(user_policy).to permit(agent, user) } + it { expect(user_policy).to permit(agent_context, user) } end end end From efc59bb43fa9e09dd08758f112c0c5bc5e4be53c Mon Sep 17 00:00:00 2001 From: Pranav Raj S Date: Tue, 26 May 2020 23:36:42 +0530 Subject: [PATCH 31/42] Bug: Fix account_id in URLs (#894) * Bug: Fix account_id in URLs * Fix accountMixin specs --- .../widgets/conversation/ConversationCard.vue | 7 ++-- .../dashboard/i18n/locale/ca/inboxMgmt.json | 2 +- .../dashboard/i18n/locale/de/inboxMgmt.json | 2 +- .../dashboard/i18n/locale/el/inboxMgmt.json | 2 +- .../dashboard/i18n/locale/en/inboxMgmt.json | 2 +- .../dashboard/i18n/locale/fr/inboxMgmt.json | 2 +- .../dashboard/i18n/locale/ml/inboxMgmt.json | 2 +- .../dashboard/i18n/locale/nl/inboxMgmt.json | 2 +- .../dashboard/i18n/locale/pt/inboxMgmt.json | 2 +- .../dashboard/i18n/locale/ro/inboxMgmt.json | 2 +- app/javascript/dashboard/mixins/account.js | 8 ++--- .../dashboard/mixins/specs/account.spec.js | 33 +++++++++++-------- .../dashboard/settings/account/Index.vue | 13 ++++---- .../settings/inbox/channels/Facebook.vue | 6 ++-- 14 files changed, 43 insertions(+), 42 deletions(-) diff --git a/app/javascript/dashboard/components/widgets/conversation/ConversationCard.vue b/app/javascript/dashboard/components/widgets/conversation/ConversationCard.vue index 4afe61c31..aa9af83b2 100644 --- a/app/javascript/dashboard/components/widgets/conversation/ConversationCard.vue +++ b/app/javascript/dashboard/components/widgets/conversation/ConversationCard.vue @@ -75,6 +75,7 @@ export default { inboxesList: 'inboxes/getInboxes', activeInbox: 'getSelectedInbox', currentUser: 'getCurrentUser', + accountId: 'getCurrentAccountId', }), isActiveChat() { @@ -97,11 +98,7 @@ export default { methods: { cardClick(chat) { const { activeInbox } = this; - const path = conversationUrl( - this.currentUser.account_id, - activeInbox, - chat.id - ); + const path = conversationUrl(this.accountId, activeInbox, chat.id); router.push({ path: frontendURL(path) }); }, extractMessageText(chatItem) { diff --git a/app/javascript/dashboard/i18n/locale/ca/inboxMgmt.json b/app/javascript/dashboard/i18n/locale/ca/inboxMgmt.json index 5059cd491..c9c1f740d 100644 --- a/app/javascript/dashboard/i18n/locale/ca/inboxMgmt.json +++ b/app/javascript/dashboard/i18n/locale/ca/inboxMgmt.json @@ -1,7 +1,7 @@ { "INBOX_MGMT": { "HEADER": "Safates d'entrada", - "SIDEBAR_TXT": "

Safata d'entrada

Quan connecteu un lloc web o una pàgina de facebook a Chatwoot, es diu Safata d'entrada. Teniu bústies d'entrada il·limitades al vostre compte de Chatwoot.

Feu click a Afegir safata d'entrada per connectar-vos a un lloc web o a una pàgina de Facebook.

Al Tauler de control, pots veure totes les converses de totes les teves safates d'entrada en un sol lloc i respondre-les a la pestanya `Converses`.

També pots veure converses específiques per a una safata d’entrada si feu clic al nom de la safata d'entrada, al panell esquerre de la taula.

", + "SIDEBAR_TXT": "

Safata d'entrada

Quan connecteu un lloc web o una pàgina de facebook a Chatwoot, es diu Safata d'entrada. Teniu bústies d'entrada il·limitades al vostre compte de Chatwoot.

Feu click a Afegir safata d'entrada per connectar-vos a un lloc web o a una pàgina de Facebook.

Al Tauler de control, pots veure totes les converses de totes les teves safates d'entrada en un sol lloc i respondre-les a la pestanya `Converses`.

També pots veure converses específiques per a una safata d’entrada si feu clic al nom de la safata d'entrada, al panell esquerre de la taula.

", "LIST": { "404": "No hi ha cap safata d'entrada connectat a aquest compte." }, diff --git a/app/javascript/dashboard/i18n/locale/de/inboxMgmt.json b/app/javascript/dashboard/i18n/locale/de/inboxMgmt.json index 5f09f8095..fca1a7c44 100644 --- a/app/javascript/dashboard/i18n/locale/de/inboxMgmt.json +++ b/app/javascript/dashboard/i18n/locale/de/inboxMgmt.json @@ -1,7 +1,7 @@ { "INBOX_MGMT": { "HEADER": "Posteingänge", - "SIDEBAR_TXT": "

Posteingang

Wenn Sie eine Website oder eine Facebook-Seite mit Chatwoot verbinden, wird dies als Posteingang bezeichnet. Sie können unbegrenzt Posteingänge in Ihrem Chatwoot-Konto haben.

Klicken Sie auf Posteingang hinzufügen , um eine Website oder eine Facebook-Seite zu verbinden.

Im Dashboard können Sie alle Konversationen aus all Ihren Posteingängen an einem einzigen Ort anzeigen und unter 'Konversationen' darauf antworten `tab.

Sie können Konversationen auch für einen Posteingang anzeigen, indem Sie auf den Namen des Posteingangs im linken Bereich des Dashboards klicken.

", + "SIDEBAR_TXT": "

Posteingang

Wenn Sie eine Website oder eine Facebook-Seite mit Chatwoot verbinden, wird dies als Posteingang bezeichnet. Sie können unbegrenzt Posteingänge in Ihrem Chatwoot-Konto haben.

Klicken Sie auf Posteingang hinzufügen , um eine Website oder eine Facebook-Seite zu verbinden.

Im Dashboard können Sie alle Konversationen aus all Ihren Posteingängen an einem einzigen Ort anzeigen und unter 'Konversationen' darauf antworten `tab.

Sie können Konversationen auch für einen Posteingang anzeigen, indem Sie auf den Namen des Posteingangs im linken Bereich des Dashboards klicken.

", "LIST": { "404": "Diesem Konto sind keine Posteingänge zugeordnet." }, diff --git a/app/javascript/dashboard/i18n/locale/el/inboxMgmt.json b/app/javascript/dashboard/i18n/locale/el/inboxMgmt.json index fe29aaebb..0a18b005b 100644 --- a/app/javascript/dashboard/i18n/locale/el/inboxMgmt.json +++ b/app/javascript/dashboard/i18n/locale/el/inboxMgmt.json @@ -1,7 +1,7 @@ { "INBOX_MGMT": { "HEADER": "Κιβώτια Εισερχομένων", - "SIDEBAR_TXT": "

Κιβώτιο Εισερχομένων

Όταν συνδέετε σε μια Ιστοσελίδα ή μια σελίδα του Facebook με το Chatwoot, δημιουργείται ένα Κιβώτιο. Μπορείτε να έχετε απεριόριστα κιβώτια στον λογαριασμό σας στο Chatwoot.

Πατήστε στο Προσθήκη Κιβωτίου για να το συνδέσετε με μια ιστοσελίδα ή μια σελίδα στο Facebook.

Στον Πίνακα Ελέγχου, μπορείτε να δείτε τις συνομιλίες από όλα τα κιβώτια σε ένα μέρος και να απαντήσετε από την καρτέλα `Συνομιλίες`.

Μπορείτε επίσης να δείτε τις συνομιλίες από ένα κιβώτιο εισερχομένων πατώντας στο όνομά του στο αριστερό μέρος του πίνακα ελέγχου.

", + "SIDEBAR_TXT": "

Κιβώτιο Εισερχομένων

Όταν συνδέετε σε μια Ιστοσελίδα ή μια σελίδα του Facebook με το Chatwoot, δημιουργείται ένα Κιβώτιο. Μπορείτε να έχετε απεριόριστα κιβώτια στον λογαριασμό σας στο Chatwoot.

Πατήστε στο Προσθήκη Κιβωτίου για να το συνδέσετε με μια ιστοσελίδα ή μια σελίδα στο Facebook.

Στον Πίνακα Ελέγχου, μπορείτε να δείτε τις συνομιλίες από όλα τα κιβώτια σε ένα μέρος και να απαντήσετε από την καρτέλα `Συνομιλίες`.

Μπορείτε επίσης να δείτε τις συνομιλίες από ένα κιβώτιο εισερχομένων πατώντας στο όνομά του στο αριστερό μέρος του πίνακα ελέγχου.

", "LIST": { "404": "Δεν υπάρχουν κιβώτια εισερχομένων σε αυτόν τον λογαριασμό." }, diff --git a/app/javascript/dashboard/i18n/locale/en/inboxMgmt.json b/app/javascript/dashboard/i18n/locale/en/inboxMgmt.json index 2aa1c15c2..da8465626 100644 --- a/app/javascript/dashboard/i18n/locale/en/inboxMgmt.json +++ b/app/javascript/dashboard/i18n/locale/en/inboxMgmt.json @@ -1,7 +1,7 @@ { "INBOX_MGMT": { "HEADER": "Inboxes", - "SIDEBAR_TXT": "

Inbox

When you connect a website or a facebook Page to Chatwoot, it is called an Inbox. You can have unlimited inboxes in your Chatwoot account.

Click on Add Inbox to connect a website or a Facebook Page.

In the Dashboard, you can see all the conversations from all your inboxes in a single place and respond to them under the `Conversations` tab.

You can also see conversations specific to an inbox by clicking on the inbox name on the left pane of the dashboard.

", + "SIDEBAR_TXT": "

Inbox

When you connect a website or a facebook Page to Chatwoot, it is called an Inbox. You can have unlimited inboxes in your Chatwoot account.

Click on Add Inbox to connect a website or a Facebook Page.

In the Dashboard, you can see all the conversations from all your inboxes in a single place and respond to them under the `Conversations` tab.

You can also see conversations specific to an inbox by clicking on the inbox name on the left pane of the dashboard.

", "LIST": { "404": "There are no inboxes attached to this account." }, diff --git a/app/javascript/dashboard/i18n/locale/fr/inboxMgmt.json b/app/javascript/dashboard/i18n/locale/fr/inboxMgmt.json index 3a1fa2761..fe35bf261 100644 --- a/app/javascript/dashboard/i18n/locale/fr/inboxMgmt.json +++ b/app/javascript/dashboard/i18n/locale/fr/inboxMgmt.json @@ -1,7 +1,7 @@ { "INBOX_MGMT": { "HEADER": "Boîtes de réception", - "SIDEBAR_TXT": "

Boîte de réception

Lorsque vous connectez un site Web ou une page Facebook à Chatwoot, elle est appelée une Boîte de réception. Vous pouvez avoir des boîtes de réception illimitées dans votre compte Chatwoot.

Cliquez sur Ajouter Boîte de réception pour connecter un site Web ou une page Facebook.

Dans le tableau de bord, vous pouvez voir toutes les conversations de toutes vos boîtes de réception en un seul endroit et y répondre dans l'onglet `Conversations`.

Vous pouvez également voir les conversations spécifiques à une boîte de réception en cliquant sur le nom de la boîte de réception sur le volet gauche du tableau de bord.

", + "SIDEBAR_TXT": "

Boîte de réception

Lorsque vous connectez un site Web ou une page Facebook à Chatwoot, elle est appelée une Boîte de réception. Vous pouvez avoir des boîtes de réception illimitées dans votre compte Chatwoot.

Cliquez sur Ajouter Boîte de réception pour connecter un site Web ou une page Facebook.

Dans le tableau de bord, vous pouvez voir toutes les conversations de toutes vos boîtes de réception en un seul endroit et y répondre dans l'onglet `Conversations`.

Vous pouvez également voir les conversations spécifiques à une boîte de réception en cliquant sur le nom de la boîte de réception sur le volet gauche du tableau de bord.

", "LIST": { "404": "Il n'y a aucune boîte de réception associée à ce compte." }, diff --git a/app/javascript/dashboard/i18n/locale/ml/inboxMgmt.json b/app/javascript/dashboard/i18n/locale/ml/inboxMgmt.json index f9e98fba7..2f87a0f67 100644 --- a/app/javascript/dashboard/i18n/locale/ml/inboxMgmt.json +++ b/app/javascript/dashboard/i18n/locale/ml/inboxMgmt.json @@ -1,7 +1,7 @@ { "INBOX_MGMT": { "HEADER": "ഇൻ‌ബോക്സുകൾ", - "SIDEBAR_TXT": "

ഇൻ‌ബോക്സ്

ചാറ്റ് വൂട്ടിലേക്ക് നിങ്ങൾ ഒരു വെബ്‌സൈറ്റോ ഫേസ്ബുക്ക് പേജോ കണക്റ്റുചെയ്യുമ്പോൾ, അതിനെ ഇൻ‌ബോക്സ് എന്ന് വിളിക്കുന്നു. നിങ്ങളുടെ ചാറ്റ് വൂട്ട് അക്കൗണ്ടിൽ പരിധിയില്ലാത്ത ഇൻബോക്സുകൾ ഉണ്ടായിരിക്കാൻ‌ കഴിയും.

ഒരു വെബ്‌സൈറ്റ് അല്ലെങ്കിൽ‌ ഫേസ്ബുക് പേജ് ബന്ധിപ്പിക്കുന്നതിന് ഇൻ‌ബോക്സ് ചേർക്കുക ക്ലിക്കുചെയ്യുക.

ഡാഷ്‌ബോർഡ് , നിങ്ങളുടെ എല്ലാ ഇൻ‌ബോക്‌സുകളിൽ‌ നിന്നുമുള്ള എല്ലാ സംഭാഷണങ്ങളും ഒരൊറ്റ സ്ഥലത്ത് കാണാനും `സംഭാഷണങ്ങൾ‌` ടാബിന് കീഴിൽ അവയോട് പ്രതികരിക്കാനും കഴിയും.

ഡാഷ്‌ബോർഡിന്റെ ഇടത് പാളിയിലെ ഇൻ‌ബോക്സ് ബട്ടണിൽ ക്ലിക്കുചെയ്ത് ഇൻബോക്സിൽ ഉള്ള സംഭാഷണങ്ങൾ കാണാൻ നിങ്ങൾക്കു സാധിക്കും .

", + "SIDEBAR_TXT": "

ഇൻ‌ബോക്സ്

ചാറ്റ് വൂട്ടിലേക്ക് നിങ്ങൾ ഒരു വെബ്‌സൈറ്റോ ഫേസ്ബുക്ക് പേജോ കണക്റ്റുചെയ്യുമ്പോൾ, അതിനെ ഇൻ‌ബോക്സ് എന്ന് വിളിക്കുന്നു. നിങ്ങളുടെ ചാറ്റ് വൂട്ട് അക്കൗണ്ടിൽ പരിധിയില്ലാത്ത ഇൻബോക്സുകൾ ഉണ്ടായിരിക്കാൻ‌ കഴിയും.

ഒരു വെബ്‌സൈറ്റ് അല്ലെങ്കിൽ‌ ഫേസ്ബുക് പേജ് ബന്ധിപ്പിക്കുന്നതിന് ഇൻ‌ബോക്സ് ചേർക്കുക ക്ലിക്കുചെയ്യുക.

ഡാഷ്‌ബോർഡ്, നിങ്ങളുടെ എല്ലാ ഇൻ‌ബോക്‌സുകളിൽ‌ നിന്നുമുള്ള എല്ലാ സംഭാഷണങ്ങളും ഒരൊറ്റ സ്ഥലത്ത് കാണാനും `സംഭാഷണങ്ങൾ‌` ടാബിന് കീഴിൽ അവയോട് പ്രതികരിക്കാനും കഴിയും.

ഡാഷ്‌ബോർഡിന്റെ ഇടത് പാളിയിലെ ഇൻ‌ബോക്സ് ബട്ടണിൽ ക്ലിക്കുചെയ്ത് ഇൻബോക്സിൽ ഉള്ള സംഭാഷണങ്ങൾ കാണാൻ നിങ്ങൾക്കു സാധിക്കും .

", "LIST": { "404": "ഈ അക്കൗണ്ടിലേക്കു ഇൻ‌ബോക്സുകളൊന്നും ബന്ധിപ്പിച്ചിട്ടില്ല." }, diff --git a/app/javascript/dashboard/i18n/locale/nl/inboxMgmt.json b/app/javascript/dashboard/i18n/locale/nl/inboxMgmt.json index 9d3734621..981ca5ffc 100644 --- a/app/javascript/dashboard/i18n/locale/nl/inboxMgmt.json +++ b/app/javascript/dashboard/i18n/locale/nl/inboxMgmt.json @@ -1,7 +1,7 @@ { "INBOX_MGMT": { "HEADER": "Inboxen", - "SIDEBAR_TXT": "

Inboxen

Wanneer u een website of een Facebook-pagina verbindt met Chatwoot, het heet een inbox. U kunt een oneindig aantal inboxen hebben in uw Chatwoot account.

Klik op Voeg Postvak in toe om een website of een Facebook-pagina te verbinden.

in het Dashboard, u kunt alle gesprekken van al uw inboxen op één plek zien en erop reageren onder het `Conversations` tabblad.

U kunt ook gesprekken zien die specifiek zijn voor een inbox door te klikken op de naam van de inbox op het linkerpaneel van het dashboard.

", + "SIDEBAR_TXT": "

Inboxen

Wanneer u een website of een Facebook-pagina verbindt met Chatwoot, het heet een inbox. U kunt een oneindig aantal inboxen hebben in uw Chatwoot account.

Klik op Voeg Postvak in toe om een website of een Facebook-pagina te verbinden.

in het Dashboard, u kunt alle gesprekken van al uw inboxen op één plek zien en erop reageren onder het `Conversations` tabblad.

U kunt ook gesprekken zien die specifiek zijn voor een inbox door te klikken op de naam van de inbox op het linkerpaneel van het dashboard.

", "LIST": { "404": "Er zijn geen inboxen aan dit account gekoppeld." }, diff --git a/app/javascript/dashboard/i18n/locale/pt/inboxMgmt.json b/app/javascript/dashboard/i18n/locale/pt/inboxMgmt.json index acd204b5c..631470370 100644 --- a/app/javascript/dashboard/i18n/locale/pt/inboxMgmt.json +++ b/app/javascript/dashboard/i18n/locale/pt/inboxMgmt.json @@ -1,7 +1,7 @@ { "INBOX_MGMT": { "HEADER": "Caixas de Entrada", - "SIDEBAR_TXT": "

Caixa de entrada

Quando você conectar um site ou uma página de Facebook ao Chatwoot, é chamado de caixa de entrada. Você pode ter caixas de entrada ilimitadas na sua conta de Chatwoot.

Clique em Adicionar caixa de entrada para conectar um site ou uma página do Facebook.

No Painel, você pode ver todas as conversas de todas as suas caixas de entrada em um único lugar e responder a elas sob a guia `Conversations`.

Você também pode ver conversas específicas para uma caixa de entrada, clicando no nome da caixa de entrada no painel esquerdo do painel.

", + "SIDEBAR_TXT": "

Caixa de entrada

Quando você conectar um site ou uma página de Facebook ao Chatwoot, é chamado de caixa de entrada. Você pode ter caixas de entrada ilimitadas na sua conta de Chatwoot.

Clique em Adicionar caixa de entrada para conectar um site ou uma página do Facebook.

No Painel, você pode ver todas as conversas de todas as suas caixas de entrada em um único lugar e responder a elas sob a guia `Conversations`.

Você também pode ver conversas específicas para uma caixa de entrada, clicando no nome da caixa de entrada no painel esquerdo do painel.

", "LIST": { "404": "Não há caixas de entrada anexadas a esta conta." }, diff --git a/app/javascript/dashboard/i18n/locale/ro/inboxMgmt.json b/app/javascript/dashboard/i18n/locale/ro/inboxMgmt.json index 344436cc6..702a3b1d5 100644 --- a/app/javascript/dashboard/i18n/locale/ro/inboxMgmt.json +++ b/app/javascript/dashboard/i18n/locale/ro/inboxMgmt.json @@ -1,7 +1,7 @@ { "INBOX_MGMT": { "HEADER": "Căsuțe", - "SIDEBAR_TXT": "

Inbox

Când conectezi un site web sau o pagină de Facebook la Chatwoot, se numește Inbox. Poți avea inbox-uri nelimitate în contul tău din Chatwoot.

Faceţi clic pe Adăugaţi Inbox pentru a conecta un website sau o pagină Facebook.

În Panoul de control , poți vedea toate conversațiile din toate cutiile tale într-un singur loc și să le răspunzi sub fila `Conversations`.

De asemenea, poți vedea conversații specifice unei căsuțe poștale făcând clic pe numele inbox-ului din panoul stâng al tabloului de bord.

", + "SIDEBAR_TXT": "

Inbox

Când conectezi un site web sau o pagină de Facebook la Chatwoot, se numește Inbox. Poți avea inbox-uri nelimitate în contul tău din Chatwoot.

Faceţi clic pe Adăugaţi Inbox pentru a conecta un website sau o pagină Facebook.

În Panoul de control, poți vedea toate conversațiile din toate cutiile tale într-un singur loc și să le răspunzi sub fila `Conversations`.

De asemenea, poți vedea conversații specifice unei căsuțe poștale făcând clic pe numele inbox-ului din panoul stâng al tabloului de bord.

", "LIST": { "404": "Nu există căsuțe poștale atașate acestui cont." }, diff --git a/app/javascript/dashboard/mixins/account.js b/app/javascript/dashboard/mixins/account.js index 8ce8d2095..9e4a6e605 100644 --- a/app/javascript/dashboard/mixins/account.js +++ b/app/javascript/dashboard/mixins/account.js @@ -1,10 +1,10 @@ -import auth from '../api/auth'; +import { mapGetters } from 'vuex'; export default { computed: { - accountId() { - return auth.getCurrentUser().account_id; - }, + ...mapGetters({ + accountId: 'getCurrentAccountId', + }), }, methods: { addAccountScoping(url) { diff --git a/app/javascript/dashboard/mixins/specs/account.spec.js b/app/javascript/dashboard/mixins/specs/account.spec.js index a0cbf0d0a..c918ae774 100644 --- a/app/javascript/dashboard/mixins/specs/account.spec.js +++ b/app/javascript/dashboard/mixins/specs/account.spec.js @@ -1,33 +1,40 @@ -import { createWrapper } from '@vue/test-utils'; +import { shallowMount, createLocalVue } from '@vue/test-utils'; import accountMixin from '../account'; -import Vue from 'vue'; +import Vuex from 'vuex'; -jest.mock('../../api/auth', () => ({ - getCurrentUser: () => ({ account_id: 1 }), -})); +const localVue = createLocalVue(); +localVue.use(Vuex); describe('accountMixin', () => { - test('set accountId properly', () => { + let getters; + let store; + + beforeEach(() => { + getters = { + getCurrentAccountId: () => 1, + }; + + store = new Vuex.Store({ getters }); + }); + + it('set accountId properly', () => { const Component = { render() {}, title: 'TestComponent', mixins: [accountMixin], }; - const Constructor = Vue.extend(Component); - const vm = new Constructor().$mount(); - const wrapper = createWrapper(vm); + const wrapper = shallowMount(Component, { store, localVue }); expect(wrapper.vm.accountId).toBe(1); }); - test('returns current url', () => { + it('returns current url', () => { const Component = { render() {}, title: 'TestComponent', mixins: [accountMixin], }; - const Constructor = Vue.extend(Component); - const vm = new Constructor().$mount(); - const wrapper = createWrapper(vm); + + const wrapper = shallowMount(Component, { store, localVue }); expect(wrapper.vm.addAccountScoping('settings/inboxes/new')).toBe( '/app/accounts/1/settings/inboxes/new' ); diff --git a/app/javascript/dashboard/routes/dashboard/settings/account/Index.vue b/app/javascript/dashboard/routes/dashboard/settings/account/Index.vue index f4bac9d12..d8d86a052 100644 --- a/app/javascript/dashboard/routes/dashboard/settings/account/Index.vue +++ b/app/javascript/dashboard/routes/dashboard/settings/account/Index.vue @@ -94,12 +94,12 @@ import Vue from 'vue'; import { required } from 'vuelidate/lib/validators'; import { mapGetters } from 'vuex'; -import { accountIdFromPathname } from 'dashboard/helper/URLHelper'; import alertMixin from 'shared/mixins/alertMixin'; import configMixin from 'shared/mixins/configMixin'; +import accountMixin from '../../../../mixins/account'; export default { - mixins: [alertMixin, configMixin], + mixins: [accountMixin, alertMixin, configMixin], data() { return { id: '', @@ -140,10 +140,7 @@ export default { }, methods: { async initializeAccount() { - const { pathname } = window.location; - const accountId = accountIdFromPathname(pathname); - - if (accountId) { + try { await this.$store.dispatch('accounts/get'); const { name, @@ -153,7 +150,7 @@ export default { support_email, domain_emails_enabled, features, - } = this.getAccount(accountId); + } = this.getAccount(this.accountId); Vue.config.lang = locale; this.name = name; @@ -163,6 +160,8 @@ export default { this.supportEmail = support_email; this.domainEmailsEnabled = domain_emails_enabled; this.features = features; + } catch (error) { + // Ignore error } }, diff --git a/app/javascript/dashboard/routes/dashboard/settings/inbox/channels/Facebook.vue b/app/javascript/dashboard/routes/dashboard/settings/inbox/channels/Facebook.vue index 282096616..bcd9cda9d 100644 --- a/app/javascript/dashboard/routes/dashboard/settings/inbox/channels/Facebook.vue +++ b/app/javascript/dashboard/routes/dashboard/settings/inbox/channels/Facebook.vue @@ -82,13 +82,14 @@ import ChannelApi from '../../../../../api/channels'; import PageHeader from '../../SettingsSubPageHeader'; import router from '../../../../index'; import globalConfigMixin from 'shared/mixins/globalConfigMixin'; +import accountMixin from '../../../../../mixins/account'; export default { components: { LoadingState, PageHeader, }, - mixins: [globalConfigMixin], + mixins: [globalConfigMixin, accountMixin], data() { return { isCreating: false, @@ -126,9 +127,6 @@ export default { currentUser: 'getCurrentUser', globalConfig: 'globalConfig/get', }), - accountId() { - return this.currentUser.account_id; - }, }, created() { From a9d93b750c6af63ac7e63bbf767e1179acb78d1d Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Fri, 29 May 2020 11:14:27 +0530 Subject: [PATCH 32/42] Chore: Bump kaminari from 1.2.0 to 1.2.1 (#898) Bumps [kaminari](https://github.com/kaminari/kaminari) from 1.2.0 to 1.2.1. - [Release notes](https://github.com/kaminari/kaminari/releases) - [Changelog](https://github.com/kaminari/kaminari/blob/master/CHANGELOG.md) - [Commits](https://github.com/kaminari/kaminari/compare/v1.2.0...v1.2.1) Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- Gemfile.lock | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/Gemfile.lock b/Gemfile.lock index ba424cac0..48e4e12a8 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -249,18 +249,18 @@ GEM json (2.3.0) json_pure (2.3.0) jwt (2.2.1) - kaminari (1.2.0) + kaminari (1.2.1) activesupport (>= 4.1.0) - kaminari-actionview (= 1.2.0) - kaminari-activerecord (= 1.2.0) - kaminari-core (= 1.2.0) - kaminari-actionview (1.2.0) + kaminari-actionview (= 1.2.1) + kaminari-activerecord (= 1.2.1) + kaminari-core (= 1.2.1) + kaminari-actionview (1.2.1) actionview - kaminari-core (= 1.2.0) - kaminari-activerecord (1.2.0) + kaminari-core (= 1.2.1) + kaminari-activerecord (1.2.1) activerecord - kaminari-core (= 1.2.0) - kaminari-core (1.2.0) + kaminari-core (= 1.2.1) + kaminari-core (1.2.1) koala (3.0.0) addressable faraday From 10a1758261d96fbc2e0ad2474aa9185a6a777729 Mon Sep 17 00:00:00 2001 From: Ronald Walker Date: Fri, 29 May 2020 00:07:18 -0700 Subject: [PATCH 33/42] Bug: Fix false positive in #toggleAgentTypingStatus spec (#899) --- .../widget/store/modules/specs/conversation/mutations.spec.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/javascript/widget/store/modules/specs/conversation/mutations.spec.js b/app/javascript/widget/store/modules/specs/conversation/mutations.spec.js index 7241dbc2f..1ce076638 100644 --- a/app/javascript/widget/store/modules/specs/conversation/mutations.spec.js +++ b/app/javascript/widget/store/modules/specs/conversation/mutations.spec.js @@ -101,7 +101,7 @@ describe('#mutations', () => { }); it('sets isAgentTyping flag to false', () => { - const state = { uiFlags: { isAgentTyping: false } }; + const state = { uiFlags: { isAgentTyping: true } }; mutations.toggleAgentTypingStatus(state, { status: 'off' }); expect(state.uiFlags.isAgentTyping).toEqual(false); }); From ec197b077d6da658e21ea16f1e152da25c7c8f86 Mon Sep 17 00:00:00 2001 From: Pranav Raj S Date: Fri, 29 May 2020 14:53:27 +0530 Subject: [PATCH 34/42] Chore: Add db migrate to Heroku release phase (#901) --- Procfile | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/Procfile b/Procfile index 16a5cc0c1..34f447dda 100644 --- a/Procfile +++ b/Procfile @@ -1,2 +1,3 @@ +release: bundle exec rake db:migrate web: bin/rails server -p $PORT -e $RAILS_ENV -worker: bundle exec sidekiq -C config/sidekiq.yml \ No newline at end of file +worker: bundle exec sidekiq -C config/sidekiq.yml From 47ec7ad7c917dbb5a3e5c0e81b2d629036605cc3 Mon Sep 17 00:00:00 2001 From: Pranav Raj S Date: Sat, 30 May 2020 17:28:00 +0530 Subject: [PATCH 35/42] Feature: Ability to customise widget color (#903) - Use Chrome style color-picker --- .../dashboard/assets/scss/_woot.scss | 1 - .../assets/scss/widgets/_colorpicker.scss | 13 --- .../dashboard/components/SettingsSection.vue | 4 +- app/javascript/dashboard/components/index.js | 2 + .../components/widgets/ColorPicker.vue | 80 +++++++++++++++++++ .../dashboard/settings/inbox/Settings.vue | 60 ++++++-------- .../settings/inbox/channels/Website.vue | 24 +++--- .../shared/helpers/vuex/mutationHelpers.js | 4 +- package.json | 2 +- yarn.lock | 2 +- 10 files changed, 124 insertions(+), 68 deletions(-) delete mode 100644 app/javascript/dashboard/assets/scss/widgets/_colorpicker.scss create mode 100644 app/javascript/dashboard/components/widgets/ColorPicker.vue diff --git a/app/javascript/dashboard/assets/scss/_woot.scss b/app/javascript/dashboard/assets/scss/_woot.scss index 05d822b5a..947204ca6 100644 --- a/app/javascript/dashboard/assets/scss/_woot.scss +++ b/app/javascript/dashboard/assets/scss/_woot.scss @@ -5,7 +5,6 @@ @import 'foundation-custom'; @import 'widgets/billing'; @import 'widgets/buttons'; -@import 'widgets/colorpicker'; @import 'widgets/conv-header'; @import 'widgets/conversation-card'; @import 'widgets/conversation-view'; diff --git a/app/javascript/dashboard/assets/scss/widgets/_colorpicker.scss b/app/javascript/dashboard/assets/scss/widgets/_colorpicker.scss deleted file mode 100644 index 9d761807a..000000000 --- a/app/javascript/dashboard/assets/scss/widgets/_colorpicker.scss +++ /dev/null @@ -1,13 +0,0 @@ -@import '~dashboard/assets/scss/variables'; - -.widget-color--selector.vc-compact { - border: 1px solid $color-border; - box-shadow: none; - margin-bottom: $space-normal; - width: 356px; - - .vc-compact-color-item { - height: 24px; - width: 24px; - } -} diff --git a/app/javascript/dashboard/components/SettingsSection.vue b/app/javascript/dashboard/components/SettingsSection.vue index ebf79bf3e..adf6f9912 100644 --- a/app/javascript/dashboard/components/SettingsSection.vue +++ b/app/javascript/dashboard/components/SettingsSection.vue @@ -1,6 +1,6 @@ From d4783db6fd738274cb5f6e227d57c431a5902feb Mon Sep 17 00:00:00 2001 From: Pranav Raj S Date: Sun, 31 May 2020 21:49:28 +0530 Subject: [PATCH 38/42] Chore: Update app logo, add FRONTEND_URL env var (#910) Chore: Update app logo, add FRONTEND_URL env var --- app.json | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/app.json b/app.json index 0b00d971e..300f363db 100644 --- a/app.json +++ b/app.json @@ -3,7 +3,7 @@ "description": "Chatwoot is a customer support tool for instant messaging channels", "website": "https://www.chatwoot.com/", "repository": "https://github.com/chatwoot/chatwoot", - "logo": "data:image/jpeg;base64,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", + "logo": "https://app.chatwoot.com/brand-assets/logo_thumbnail.svg", "keywords": [ "live chat", "customer support", @@ -27,6 +27,10 @@ "RAILS_ENV": { "description": "Environment for rails middleware.", "value": "production" + }, + "FRONTEND_URL": { + "description": "Public root URL of the Chatwoot installation. This will be used in the emails.", + "value": "https://CHANGE.herokuapp.com" } }, "formation": { From 8c52a3a9531372f642d0be1ec5cbd283b98129d4 Mon Sep 17 00:00:00 2001 From: Sojan Jose Date: Sun, 31 May 2020 23:10:20 +0530 Subject: [PATCH 39/42] Chore: Remove duplication of user JBuilder (#911) * Chore: Reduce duplication of user JBuilder --- .../api/v1/accounts/agents_controller.rb | 4 ++-- app/controllers/api/v1/profiles_controller.rb | 2 +- .../v1/accounts/agents/index.json.jbuilder | 9 +------- app/views/api/v1/models/_agent.json.jbuilder | 8 +++++++ app/views/api/v1/models/_user.json.jbuilder | 21 +++++++++++++++++++ app/views/api/v1/models/user.json.jbuilder | 12 ----------- .../api/v1/profiles/update.json.jbuilder | 10 +-------- app/views/devise/_auth.json.jbuilder | 21 +------------------ .../v1/accounts/accounts_controller_spec.rb | 4 +++- .../api/v1/profiles_controller_spec.rb | 1 + 10 files changed, 39 insertions(+), 53 deletions(-) create mode 100644 app/views/api/v1/models/_agent.json.jbuilder create mode 100644 app/views/api/v1/models/_user.json.jbuilder delete mode 100644 app/views/api/v1/models/user.json.jbuilder diff --git a/app/controllers/api/v1/accounts/agents_controller.rb b/app/controllers/api/v1/accounts/agents_controller.rb index 7cdd6dbb5..85a05fbc1 100644 --- a/app/controllers/api/v1/accounts/agents_controller.rb +++ b/app/controllers/api/v1/accounts/agents_controller.rb @@ -17,11 +17,11 @@ class Api::V1::Accounts::AgentsController < Api::BaseController def update @agent.update!(agent_params.except(:role)) @agent.current_account_user.update!(role: agent_params[:role]) if agent_params[:role] - render 'api/v1/models/user.json', locals: { resource: @agent } + render partial: 'api/v1/models/agent.json.jbuilder', locals: { resource: @agent } end def create - render 'api/v1/models/user.json', locals: { resource: @user } + render partial: 'api/v1/models/agent.json.jbuilder', locals: { resource: @user } end private diff --git a/app/controllers/api/v1/profiles_controller.rb b/app/controllers/api/v1/profiles_controller.rb index 9a0bbfc17..bd415ffeb 100644 --- a/app/controllers/api/v1/profiles_controller.rb +++ b/app/controllers/api/v1/profiles_controller.rb @@ -2,7 +2,7 @@ class Api::V1::ProfilesController < Api::BaseController before_action :set_user def show - render json: @user + render partial: 'api/v1/models/user.json.jbuilder', locals: { resource: @user } end def update diff --git a/app/views/api/v1/accounts/agents/index.json.jbuilder b/app/views/api/v1/accounts/agents/index.json.jbuilder index 0b6a01f1c..8374ce793 100644 --- a/app/views/api/v1/accounts/agents/index.json.jbuilder +++ b/app/views/api/v1/accounts/agents/index.json.jbuilder @@ -1,10 +1,3 @@ json.array! @agents do |agent| - json.account_id agent.account.id - json.availability_status agent.availability_status - json.confirmed agent.confirmed? - json.email agent.email - json.id agent.id - json.name agent.name - json.role agent.role - json.thumbnail agent.avatar_url + json.partial! 'api/v1/models/agent.json.jbuilder', resource: agent end diff --git a/app/views/api/v1/models/_agent.json.jbuilder b/app/views/api/v1/models/_agent.json.jbuilder new file mode 100644 index 000000000..a53b1f2cc --- /dev/null +++ b/app/views/api/v1/models/_agent.json.jbuilder @@ -0,0 +1,8 @@ +json.account_id resource.account.id +json.availability_status resource.availability_status +json.confirmed resource.confirmed? +json.email resource.email +json.id resource.id +json.name resource.name +json.role resource.role +json.thumbnail resource.avatar_url diff --git a/app/views/api/v1/models/_user.json.jbuilder b/app/views/api/v1/models/_user.json.jbuilder new file mode 100644 index 000000000..4fdacf14a --- /dev/null +++ b/app/views/api/v1/models/_user.json.jbuilder @@ -0,0 +1,21 @@ +json.id resource.id +json.provider resource.provider +json.uid resource.uid +json.name resource.name +json.nickname resource.nickname +json.email resource.email +json.account_id resource.active_account_user.account_id +json.pubsub_token resource.pubsub_token +json.role resource.active_account_user.role +json.inviter_id resource.active_account_user.inviter_id +json.confirmed resource.confirmed? +json.avatar_url resource.avatar_url +json.access_token resource.access_token.token +json.accounts do + json.array! resource.account_users do |account_user| + json.id account_user.account_id + json.name account_user.account.name + json.active_at account_user.active_at + json.role account_user.role + end +end diff --git a/app/views/api/v1/models/user.json.jbuilder b/app/views/api/v1/models/user.json.jbuilder deleted file mode 100644 index d12a35657..000000000 --- a/app/views/api/v1/models/user.json.jbuilder +++ /dev/null @@ -1,12 +0,0 @@ -json.id resource.id -json.provider resource.provider -json.uid resource.uid -json.name resource.name -json.nickname resource.nickname -json.email resource.email -json.account_id resource.current_account_user.account_id -json.pubsub_token resource.pubsub_token -json.role resource.current_account_user.role -json.inviter_id resource.current_account_user.inviter_id -json.confirmed resource.confirmed? -json.avatar_url resource.avatar_url diff --git a/app/views/api/v1/profiles/update.json.jbuilder b/app/views/api/v1/profiles/update.json.jbuilder index f4eef3516..5a6dc2dad 100644 --- a/app/views/api/v1/profiles/update.json.jbuilder +++ b/app/views/api/v1/profiles/update.json.jbuilder @@ -1,9 +1 @@ -json.id @user.id -json.provider @user.provider -json.uid @user.uid -json.name @user.name -json.nickname @user.nickname -json.email @user.email -json.pubsub_token @user.pubsub_token -json.confirmed @user.confirmed? -json.avatar_url @user.avatar_url +json.partial! 'api/v1/models/user.json.jbuilder', resource: @user diff --git a/app/views/devise/_auth.json.jbuilder b/app/views/devise/_auth.json.jbuilder index a5035d7a1..3c8c11d52 100644 --- a/app/views/devise/_auth.json.jbuilder +++ b/app/views/devise/_auth.json.jbuilder @@ -1,22 +1,3 @@ json.data do - json.id resource.id - json.provider resource.provider - json.uid resource.uid - json.name resource.name - json.nickname resource.nickname - json.email resource.email - json.account_id resource.active_account_user.account_id - json.pubsub_token resource.pubsub_token - json.role resource.active_account_user.role - json.inviter_id resource.active_account_user.inviter_id - json.confirmed resource.confirmed? - json.avatar_url resource.avatar_url - json.accounts do - json.array! resource.account_users do |account_user| - json.id account_user.account_id - json.name account_user.account.name - json.active_at account_user.active_at - json.role account_user.role - end - end + json.partial! 'api/v1/models/user.json.jbuilder', resource: resource end diff --git a/spec/controllers/api/v1/accounts/accounts_controller_spec.rb b/spec/controllers/api/v1/accounts/accounts_controller_spec.rb index 30bffd9f2..17eea3043 100644 --- a/spec/controllers/api/v1/accounts/accounts_controller_spec.rb +++ b/spec/controllers/api/v1/accounts/accounts_controller_spec.rb @@ -70,8 +70,10 @@ RSpec.describe 'Accounts API', type: :request do headers: { api_access_token: super_admin.access_token.token }, as: :json - expect(User.find_by(email: email).confirmed?).to eq(true) + created_user = User.find_by(email: email) + expect(created_user.confirmed?).to eq(true) expect(response.headers.keys).to include('access-token', 'token-type', 'client', 'expiry', 'uid') + expect(response.body).to include(created_user.access_token.token) end end diff --git a/spec/controllers/api/v1/profiles_controller_spec.rb b/spec/controllers/api/v1/profiles_controller_spec.rb index 9355177ed..a2226d68b 100644 --- a/spec/controllers/api/v1/profiles_controller_spec.rb +++ b/spec/controllers/api/v1/profiles_controller_spec.rb @@ -24,6 +24,7 @@ RSpec.describe 'Profile API', type: :request do json_response = JSON.parse(response.body) expect(json_response['id']).to eq(agent.id) expect(json_response['email']).to eq(agent.email) + expect(json_response['access_token']).to eq(agent.access_token.token) end end end From f78df91dd2edaf580ed3ebc79dfae96eff26fdaa Mon Sep 17 00:00:00 2001 From: Pranav Raj S Date: Tue, 2 Jun 2020 22:59:02 +0530 Subject: [PATCH 40/42] Chore: Contact Sidebar, conversation cleanup (#908) - Update sidebar design - Move every contact data to contacts module - Revert go to next conversation feature - Fix issues with new conversation in action cable - Escape HTML content - Broadcast event when conversation.contact changes. Co-authored-by: Sojan --- .../assets/scss/plugins/_multiselect.scss | 3 - .../widgets/conversation/ConversationCard.vue | 31 +++++-- .../conversation/ConversationHeader.vue | 15 +++- .../dashboard/helper/actionCable.js | 12 +++ .../conversation/ContactConversations.vue | 9 ++- .../conversation/ContactDetailsItem.vue | 2 +- .../dashboard/conversation/ContactPanel.vue | 69 +++++++++++----- .../conversation/ConversationLabels.vue | 81 ++++++++++--------- .../dashboard/store/modules/contacts.js | 28 ++++--- .../store/modules/conversations/actions.js | 28 +++++-- .../store/modules/conversations/getters.js | 11 +-- .../store/modules/conversations/index.js | 10 +++ .../modules/specs/contacts/getters.spec.js | 6 +- .../modules/specs/contacts/mutations.spec.js | 30 ++++--- .../specs/conversations/actions.spec.js | 10 ++- .../dashboard/store/mutation-types.js | 1 + .../shared/helpers/HTMLSanitizer.js | 8 ++ .../shared/helpers/MessageFormatter.js | 4 +- app/listeners/action_cable_listener.rb | 7 ++ app/models/contact.rb | 8 +- app/models/conversation.rb | 3 +- lib/events/types.rb | 1 + 22 files changed, 252 insertions(+), 125 deletions(-) create mode 100644 app/javascript/shared/helpers/HTMLSanitizer.js diff --git a/app/javascript/dashboard/assets/scss/plugins/_multiselect.scss b/app/javascript/dashboard/assets/scss/plugins/_multiselect.scss index 6d522dc9b..480e31c61 100644 --- a/app/javascript/dashboard/assets/scss/plugins/_multiselect.scss +++ b/app/javascript/dashboard/assets/scss/plugins/_multiselect.scss @@ -91,7 +91,6 @@ } .sidebar-labels-wrap { - &.has-edited, &:hover { .multiselect { @@ -108,8 +107,6 @@ } .multiselect { - margin-top: $space-small; - >.multiselect__select { visibility: hidden; } diff --git a/app/javascript/dashboard/components/widgets/conversation/ConversationCard.vue b/app/javascript/dashboard/components/widgets/conversation/ConversationCard.vue index aa9af83b2..a2a914bb2 100644 --- a/app/javascript/dashboard/components/widgets/conversation/ConversationCard.vue +++ b/app/javascript/dashboard/components/widgets/conversation/ConversationCard.vue @@ -6,15 +6,15 @@ >

- {{ chat.meta.sender.name }} + {{ currentContact.name }}

- + v-html="extractMessageText(lastMessageInChat)" + />
- {{ dynamicTime(lastMessage(chat).created_at) }} + {{ + lastMessageInChat ? dynamicTime(lastMessageInChat.created_at) : '' + }} {{ getUnreadCount }}
@@ -78,6 +79,12 @@ export default { accountId: 'getCurrentAccountId', }), + currentContact() { + return this.$store.getters['contacts/getContact']( + this.chat.meta.sender.id + ); + }, + isActiveChat() { return this.currentChat.id === this.chat.id; }, @@ -93,6 +100,10 @@ export default { isInboxNameVisible() { return !this.activeInbox; }, + + lastMessageInChat() { + return this.lastMessage(this.chat); + }, }, methods: { @@ -102,6 +113,10 @@ export default { router.push({ path: frontendURL(path) }); }, extractMessageText(chatItem) { + if (!chatItem) { + return ''; + } + const { content, attachments } = chatItem; if (content) { diff --git a/app/javascript/dashboard/components/widgets/conversation/ConversationHeader.vue b/app/javascript/dashboard/components/widgets/conversation/ConversationHeader.vue index 29215362f..8be30ff68 100644 --- a/app/javascript/dashboard/components/widgets/conversation/ConversationHeader.vue +++ b/app/javascript/dashboard/components/widgets/conversation/ConversationHeader.vue @@ -2,14 +2,14 @@

- {{ chat.meta.sender.name }} + {{ currentContact.name }}

-
+
+ +
+ {{ + $t('CONTACT_PANEL.LABELS.UPDATE_ERROR') + }} +
@@ -45,10 +49,12 @@