diff --git a/.circleci/config.yml b/.circleci/config.yml index e6394ccde..69ceb2772 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -102,8 +102,8 @@ jobs: echo "ERROR: The swagger.json file is not in sync with the yaml specification. Run 'rake swagger:build' and commit 'swagger/swagger.json'." exit 1 fi - curl -L https://repo1.maven.org/maven2/org/openapitools/openapi-generator-cli/5.3.0/openapi-generator-cli-5.3.0.jar > ~/tmp/openapi-generator-cli-5.3.0.jar - java -jar ~/tmp/openapi-generator-cli-5.3.0.jar validate -i swagger/swagger.json + curl -L https://repo1.maven.org/maven2/org/openapitools/openapi-generator-cli/6.3.0/openapi-generator-cli-6.3.0.jar > ~/tmp/openapi-generator-cli-6.3.0.jar + java -jar ~/tmp/openapi-generator-cli-6.3.0.jar validate -i swagger/swagger.json # Database setup - run: yarn install --check-files diff --git a/.devcontainer/Dockerfile.base b/.devcontainer/Dockerfile.base index 769be24f8..c0beb9eed 100644 --- a/.devcontainer/Dockerfile.base +++ b/.devcontainer/Dockerfile.base @@ -30,7 +30,7 @@ RUN apt-get update && export DEBIAN_FRONTEND=noninteractive \ npm # Install rbenv and ruby -ARG RUBY_VERSION="3.0.4" +ARG RUBY_VERSION="3.1.3" RUN git clone https://github.com/rbenv/rbenv.git ~/.rbenv \ && echo 'export PATH="$HOME/.rbenv/bin:$PATH"' >> ~/.bashrc \ && echo 'eval "$(rbenv init -)"' >> ~/.bashrc diff --git a/.devcontainer/devcontainer.json b/.devcontainer/devcontainer.json index a70ba3788..d2dac356b 100644 --- a/.devcontainer/devcontainer.json +++ b/.devcontainer/devcontainer.json @@ -25,7 +25,7 @@ // 1025,8025 mailhog "forwardPorts": [8025, 3000, 3035], - "postCreateCommand": ".devcontainer/scripts/setup.sh && bundle exec rake db:chatwoot_prepare && yarn", + "postCreateCommand": ".devcontainer/scripts/setup.sh && POSTGRES_STATEMENT_TIMEOUT=600s bundle exec rake db:chatwoot_prepare && yarn", "portsAttributes": { "3000": { "label": "Rails Server" diff --git a/.env.example b/.env.example index 149d1f6e6..ac4a2c0e8 100644 --- a/.env.example +++ b/.env.example @@ -52,6 +52,8 @@ POSTGRES_HOST=postgres POSTGRES_USERNAME=postgres POSTGRES_PASSWORD= RAILS_ENV=development +# Changes the Postgres query timeout limit. The default is 14 seconds. Modify only when required. +# POSTGRES_STATEMENT_TIMEOUT=14s RAILS_MAX_THREADS=5 # The email from which all outgoing emails are sent @@ -169,6 +171,9 @@ USE_INBOX_AVATAR_FOR_BOT=true ## Sentry # SENTRY_DSN= +## LogRocket +# LOG_ROCKET_PROJECT_ID=xxxxx/some-project + ## Scout ## https://scoutapm.com/docs/ruby/configuration # SCOUT_KEY=YOURKEY @@ -186,11 +191,7 @@ USE_INBOX_AVATAR_FOR_BOT=true ## https://github.com/DataDog/dd-trace-rb/blob/master/docs/GettingStarted.md#environment-variables # DD_TRACE_AGENT_URL= -## IP look up configuration -## ref https://github.com/alexreisner/geocoder/blob/master/README_API_GUIDE.md -## works only on accounts with ip look up feature enabled -# IP_LOOKUP_SERVICE=geoip2 -# maxmindb api key to use geoip2 service +# MaxMindDB API key to download GeoLite2 City database # IP_LOOKUP_API_KEY= ## Rack Attack configuration diff --git a/.github/workflows/nightly_installer.yml b/.github/workflows/nightly_installer.yml index 6b076cd8a..034c08e8c 100644 --- a/.github/workflows/nightly_installer.yml +++ b/.github/workflows/nightly_installer.yml @@ -44,3 +44,9 @@ jobs: # sudo systemctl restart chatwoot.target # curl http://localhost:3000/api + - name: Upload chatwoot setup log file as an artifact + uses: actions/upload-artifact@v3 + if: always() + with: + name: chatwoot-setup-log-file + path: /var/log/chatwoot-setup.log diff --git a/.github/workflows/run_foss_spec.yml b/.github/workflows/run_foss_spec.yml index 98fae5c8e..51688944c 100644 --- a/.github/workflows/run_foss_spec.yml +++ b/.github/workflows/run_foss_spec.yml @@ -15,7 +15,7 @@ on: jobs: test: - runs-on: ubuntu-latest + runs-on: ubuntu-20.04 services: postgres: image: postgres:10.8 @@ -49,6 +49,10 @@ jobs: with: bundler-cache: true # runs 'bundle install' and caches installed gems automatically + - uses: actions/setup-node@v3 + with: + node-version: 16 + - name: yarn run: yarn install @@ -70,3 +74,10 @@ jobs: - name: Run backend tests run: | bundle exec rspec --profile=10 --format documentation + + - name: Upload rails log folder + uses: actions/upload-artifact@v3 + if: always() + with: + name: rails-log-folder + path: log diff --git a/.husky/pre-commit b/.husky/pre-commit index 9984ac506..adda426ad 100755 --- a/.husky/pre-commit +++ b/.husky/pre-commit @@ -5,7 +5,7 @@ npx --no-install lint-staged # lint only staged ruby files -git diff --name-only --cached | xargs ls -1 2>/dev/null | grep '\.rb$' | xargs bundle exec rubocop -a +git diff --name-only --cached | xargs ls -1 2>/dev/null | grep '\.rb$' | xargs bundle exec rubocop --force-exclusion -a # stage rubocop changes to files git diff --name-only --cached | xargs git add diff --git a/.vscode/settings.json b/.vscode/settings.json index 9e26dfeeb..b3cfaee50 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -1 +1,6 @@ -{} \ No newline at end of file +{ + "cSpell.words": [ + "chatwoot", + "dompurify" + ] +} diff --git a/Gemfile b/Gemfile index 011acf584..7013245e5 100644 --- a/Gemfile +++ b/Gemfile @@ -4,7 +4,7 @@ ruby '3.1.3' ##-- base gems for rails --## gem 'rack-cors', require: 'rack/cors' -gem 'rails', '~> 6.1', '>= 6.1.7.1' +gem 'rails', '~> 6.1', '>= 6.1.7.3' # Reduces boot times through caching; required in config/boot.rb gem 'bootsnap', require: false @@ -96,13 +96,16 @@ gem 'slack-ruby-client' gem 'google-cloud-dialogflow' ##-- apm and error monitoring ---# -gem 'ddtrace' -gem 'elastic-apm' -gem 'newrelic_rpm' -gem 'scout_apm' -gem 'sentry-rails', '~> 5.3', '>= 5.3.1' -gem 'sentry-ruby', '~> 5.3' -gem 'sentry-sidekiq', '~> 5.3', '>= 5.3.1' +# loaded only when environment variables are set. +# ref application.rb +gem 'ddtrace', require: false +gem 'elastic-apm', require: false +gem 'newrelic_rpm', require: false +gem 'newrelic-sidekiq-metrics', require: false +gem 'scout_apm', require: false +gem 'sentry-rails', require: false +gem 'sentry-ruby', require: false +gem 'sentry-sidekiq', require: false ##-- background job processing --## gem 'sidekiq', '~> 6.4.2' @@ -203,6 +206,8 @@ end # worked with microsoft refresh token gem 'omniauth-oauth2' +gem 'audited', '~> 5.2' + # need for google auth gem 'omniauth' gem 'omniauth-google-oauth2' diff --git a/Gemfile.lock b/Gemfile.lock index 112e09e7f..9937dfcc2 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -9,63 +9,63 @@ GIT GEM remote: https://rubygems.org/ specs: - actioncable (6.1.7.1) - actionpack (= 6.1.7.1) - activesupport (= 6.1.7.1) + actioncable (6.1.7.3) + actionpack (= 6.1.7.3) + activesupport (= 6.1.7.3) nio4r (~> 2.0) websocket-driver (>= 0.6.1) - actionmailbox (6.1.7.1) - actionpack (= 6.1.7.1) - activejob (= 6.1.7.1) - activerecord (= 6.1.7.1) - activestorage (= 6.1.7.1) - activesupport (= 6.1.7.1) + actionmailbox (6.1.7.3) + actionpack (= 6.1.7.3) + activejob (= 6.1.7.3) + activerecord (= 6.1.7.3) + activestorage (= 6.1.7.3) + activesupport (= 6.1.7.3) mail (>= 2.7.1) - actionmailer (6.1.7.1) - actionpack (= 6.1.7.1) - actionview (= 6.1.7.1) - activejob (= 6.1.7.1) - activesupport (= 6.1.7.1) + actionmailer (6.1.7.3) + actionpack (= 6.1.7.3) + actionview (= 6.1.7.3) + activejob (= 6.1.7.3) + activesupport (= 6.1.7.3) mail (~> 2.5, >= 2.5.4) rails-dom-testing (~> 2.0) - actionpack (6.1.7.1) - actionview (= 6.1.7.1) - activesupport (= 6.1.7.1) + actionpack (6.1.7.3) + actionview (= 6.1.7.3) + activesupport (= 6.1.7.3) rack (~> 2.0, >= 2.0.9) rack-test (>= 0.6.3) rails-dom-testing (~> 2.0) rails-html-sanitizer (~> 1.0, >= 1.2.0) - actiontext (6.1.7.1) - actionpack (= 6.1.7.1) - activerecord (= 6.1.7.1) - activestorage (= 6.1.7.1) - activesupport (= 6.1.7.1) + actiontext (6.1.7.3) + actionpack (= 6.1.7.3) + activerecord (= 6.1.7.3) + activestorage (= 6.1.7.3) + activesupport (= 6.1.7.3) nokogiri (>= 1.8.5) - actionview (6.1.7.1) - activesupport (= 6.1.7.1) + actionview (6.1.7.3) + activesupport (= 6.1.7.3) builder (~> 3.1) erubi (~> 1.4) rails-dom-testing (~> 2.0) rails-html-sanitizer (~> 1.1, >= 1.2.0) active_record_query_trace (1.8) - activejob (6.1.7.1) - activesupport (= 6.1.7.1) + activejob (6.1.7.3) + activesupport (= 6.1.7.3) globalid (>= 0.3.6) - activemodel (6.1.7.1) - activesupport (= 6.1.7.1) - activerecord (6.1.7.1) - activemodel (= 6.1.7.1) - activesupport (= 6.1.7.1) + activemodel (6.1.7.3) + activesupport (= 6.1.7.3) + activerecord (6.1.7.3) + activemodel (= 6.1.7.3) + activesupport (= 6.1.7.3) activerecord-import (1.4.0) activerecord (>= 4.2) - activestorage (6.1.7.1) - actionpack (= 6.1.7.1) - activejob (= 6.1.7.1) - activerecord (= 6.1.7.1) - activesupport (= 6.1.7.1) + activestorage (6.1.7.3) + actionpack (= 6.1.7.3) + activejob (= 6.1.7.3) + activerecord (= 6.1.7.3) + activesupport (= 6.1.7.3) marcel (~> 1.0) mini_mime (>= 1.1.0) - activesupport (6.1.7.1) + activesupport (6.1.7.3) concurrent-ruby (~> 1.0, >= 1.0.2) i18n (>= 1.6, < 2) minitest (>= 5.1) @@ -90,6 +90,8 @@ GEM rake (>= 10.4, < 14.0) ast (2.4.2) attr_extras (6.2.5) + audited (5.2.0) + activerecord (>= 5.0, < 7.1) aws-eventstream (1.2.0) aws-partitions (1.605.0) aws-sdk-core (3.131.2) @@ -136,7 +138,7 @@ GEM climate_control (1.1.1) coderay (1.1.3) commonmarker (0.23.7) - concurrent-ruby (1.1.10) + concurrent-ruby (1.2.2) connection_pool (2.2.5) crack (0.4.5) rexml @@ -187,7 +189,7 @@ GEM concurrent-ruby (~> 1.0) http (>= 3.0) email_reply_trimmer (0.1.13) - erubi (1.10.0) + erubi (1.12.0) et-orbi (1.2.7) tzinfo execjs (2.8.1) @@ -248,7 +250,7 @@ GEM grpc (~> 1.36) geocoder (1.8.0) gli (2.21.0) - globalid (1.0.1) + globalid (1.1.0) activesupport (>= 5.0) gmail_xoauth (0.4.2) oauth (>= 0.3.6) @@ -353,7 +355,7 @@ GEM mini_mime (>= 1.0.0) multi_xml (>= 0.5.2) httpclient (2.8.3) - i18n (1.11.0) + i18n (1.12.0) concurrent-ruby (~> 1.0) image_processing (1.12.2) mini_magick (>= 4.9.5, < 5) @@ -417,8 +419,11 @@ GEM loofah (2.19.1) crass (~> 1.0.2) nokogiri (>= 1.5.9) - mail (2.7.1) + mail (2.8.1) mini_mime (>= 0.1.1) + net-imap + net-pop + net-smtp marcel (1.0.2) maxminddb (0.1.22) memoist (0.16.2) @@ -428,8 +433,8 @@ GEM mime-types-data (3.2022.0105) mini_magick (4.11.0) mini_mime (1.1.2) - mini_portile2 (2.8.0) - minitest (5.16.2) + mini_portile2 (2.8.1) + minitest (5.18.0) mock_redis (0.32.0) ruby2_keywords momentjs-rails (2.29.1.1) @@ -449,16 +454,19 @@ GEM net-smtp (0.3.3) net-protocol netrc (0.11.0) + newrelic-sidekiq-metrics (1.6.1) + newrelic_rpm (~> 8) + sidekiq newrelic_rpm (8.15.0) nio4r (2.5.8) - nokogiri (1.13.10) + nokogiri (1.14.2) mini_portile2 (~> 2.8.0) racc (~> 1.4) - nokogiri (1.13.10-arm64-darwin) + nokogiri (1.14.2-arm64-darwin) racc (~> 1.4) - nokogiri (1.13.10-x86_64-darwin) + nokogiri (1.14.2-x86_64-darwin) racc (~> 1.4) - nokogiri (1.13.10-x86_64-linux) + nokogiri (1.14.2-x86_64-linux) racc (~> 1.4) oauth (0.5.10) oauth2 (2.0.9) @@ -506,8 +514,8 @@ GEM pundit (2.2.0) activesupport (>= 3.0.0) raabro (1.4.0) - racc (1.6.1) - rack (2.2.6.2) + racc (1.6.2) + rack (2.2.6.4) rack-attack (6.6.1) rack (>= 1.0, < 3) rack-cors (1.1.1) @@ -516,32 +524,32 @@ GEM rack rack-proxy (0.7.2) rack - rack-test (2.0.2) + rack-test (2.1.0) rack (>= 1.3) rack-timeout (0.6.3) - rails (6.1.7.1) - actioncable (= 6.1.7.1) - actionmailbox (= 6.1.7.1) - actionmailer (= 6.1.7.1) - actionpack (= 6.1.7.1) - actiontext (= 6.1.7.1) - actionview (= 6.1.7.1) - activejob (= 6.1.7.1) - activemodel (= 6.1.7.1) - activerecord (= 6.1.7.1) - activestorage (= 6.1.7.1) - activesupport (= 6.1.7.1) + rails (6.1.7.3) + actioncable (= 6.1.7.3) + actionmailbox (= 6.1.7.3) + actionmailer (= 6.1.7.3) + actionpack (= 6.1.7.3) + actiontext (= 6.1.7.3) + actionview (= 6.1.7.3) + activejob (= 6.1.7.3) + activemodel (= 6.1.7.3) + activerecord (= 6.1.7.3) + activestorage (= 6.1.7.3) + activesupport (= 6.1.7.3) bundler (>= 1.15.0) - railties (= 6.1.7.1) + railties (= 6.1.7.3) sprockets-rails (>= 2.0.0) rails-dom-testing (2.0.3) activesupport (>= 4.2.0) nokogiri (>= 1.6) - rails-html-sanitizer (1.4.4) + rails-html-sanitizer (1.5.0) loofah (~> 2.19, >= 2.19.1) - railties (6.1.7.1) - actionpack (= 6.1.7.1) - activesupport (= 6.1.7.1) + railties (6.1.7.3) + actionpack (= 6.1.7.3) + activesupport (= 6.1.7.3) method_source rake (>= 12.2) thor (~> 1.0) @@ -676,9 +684,9 @@ GEM spring-watcher-listen (2.0.1) listen (>= 2.7, < 4.0) spring (>= 1.2, < 3.0) - sprockets (4.1.1) + sprockets (4.2.0) concurrent-ruby (~> 1.0) - rack (> 1, < 3) + rack (>= 2.2.4, < 4) sprockets-rails (3.4.2) actionpack (>= 5.2) activesupport (>= 5.2) @@ -701,7 +709,7 @@ GEM nokogiri (>= 1.6, < 2.0) twitty (0.1.4) oauth - tzinfo (2.0.4) + tzinfo (2.0.6) concurrent-ruby (~> 1.0) tzinfo-data (1.2022.1) tzinfo (>= 1.0.0) @@ -746,7 +754,7 @@ GEM working_hours (1.4.1) activesupport (>= 3.2) tzinfo - zeitwerk (2.6.0) + zeitwerk (2.6.7) PLATFORMS arm64-darwin-20 @@ -763,6 +771,7 @@ DEPENDENCIES administrate annotate attr_extras + audited (~> 5.2) aws-sdk-s3 azure-storage-blob barnes @@ -817,6 +826,7 @@ DEPENDENCIES net-imap net-pop net-smtp + newrelic-sidekiq-metrics newrelic_rpm omniauth omniauth-google-oauth2 @@ -831,7 +841,7 @@ DEPENDENCIES rack-attack rack-cors rack-timeout - rails (~> 6.1, >= 6.1.7.1) + rails (~> 6.1, >= 6.1.7.3) redis redis-namespace responders @@ -844,9 +854,9 @@ DEPENDENCIES rubocop-rspec scout_apm seed_dump - sentry-rails (~> 5.3, >= 5.3.1) - sentry-ruby (~> 5.3) - sentry-sidekiq (~> 5.3, >= 5.3.1) + sentry-rails + sentry-ruby + sentry-sidekiq shoulda-matchers sidekiq (~> 6.4.2) sidekiq-cron (~> 1.6, >= 1.6.0) diff --git a/Procfile b/Procfile index 01bfd1c1f..3cfa8ae13 100644 --- a/Procfile +++ b/Procfile @@ -1,3 +1,3 @@ -release: bundle exec rails db:chatwoot_prepare -web: bin/rails server -p $PORT -e $RAILS_ENV -worker: bundle exec sidekiq -C config/sidekiq.yml +release: POSTGRES_STATEMENT_TIMEOUT=600s bundle exec rails db:chatwoot_prepare +web: bundle exec rails ip_lookup:setup && bin/rails server -p $PORT -e $RAILS_ENV +worker: bundle exec rails ip_lookup:setup && bundle exec sidekiq -C config/sidekiq.yml diff --git a/app/assets/config/manifest.js b/app/assets/config/manifest.js index f5e0f5476..38da59e09 100644 --- a/app/assets/config/manifest.js +++ b/app/assets/config/manifest.js @@ -2,3 +2,4 @@ //= link administrate/application.css //= link administrate/application.js //= link dashboardChart.js +//= link secretField.js diff --git a/app/assets/javascripts/secretField.js b/app/assets/javascripts/secretField.js new file mode 100644 index 000000000..463109812 --- /dev/null +++ b/app/assets/javascripts/secretField.js @@ -0,0 +1,34 @@ +// eslint-disable-next-line +function toggleSecretField(e) { + e.preventDefault(); + e.stopPropagation(); + + const toggler = e.currentTarget; + const secretField = toggler.parentElement; + const textElement = secretField.querySelector('[data-secret-masked]'); + + if (!textElement) return; + + if (textElement.dataset.secretMasked === 'false') { + textElement.textContent = '•'.repeat(10); + textElement.dataset.secretMasked = 'true'; + toggler.querySelector('svg use').setAttribute('xlink:href', '#eye-show'); + + return; + } + + textElement.textContent = secretField.dataset.secretText; + textElement.dataset.secretMasked = 'false'; + toggler.querySelector('svg use').setAttribute('xlink:href', '#eye-hide'); +} + +// eslint-disable-next-line +function copySecretField(e) { + e.preventDefault(); + e.stopPropagation(); + + const toggler = e.currentTarget; + const secretField = toggler.parentElement; + + navigator.clipboard.writeText(secretField.dataset.secretText); +} diff --git a/app/assets/stylesheets/administrate/application.scss b/app/assets/stylesheets/administrate/application.scss index 79738bbf3..86c5254ec 100644 --- a/app/assets/stylesheets/administrate/application.scss +++ b/app/assets/stylesheets/administrate/application.scss @@ -6,7 +6,6 @@ @import 'utilities/text-color'; @import 'selectize'; -@import 'datetime_picker'; @import 'library/clearfix'; @import 'library/data-label'; diff --git a/app/assets/stylesheets/administrate/components/_cells.scss b/app/assets/stylesheets/administrate/components/_cells.scss index 2f7e27c4a..b5a079976 100644 --- a/app/assets/stylesheets/administrate/components/_cells.scss +++ b/app/assets/stylesheets/administrate/components/_cells.scss @@ -43,3 +43,20 @@ .cell-label--number { text-align: right; } + +.cell-data__secret-field { + align-items: center; + display: flex; + + span { + flex: 1; + } + + button { + margin-left: 5px; + + svg { + fill: currentColor; + } + } +} diff --git a/app/builders/messages/facebook/message_builder.rb b/app/builders/messages/facebook/message_builder.rb index 42d567e54..e0343c0a0 100644 --- a/app/builders/messages/facebook/message_builder.rb +++ b/app/builders/messages/facebook/message_builder.rb @@ -116,7 +116,11 @@ class Messages::Facebook::MessageBuilder < Messages::Messenger::MessageBuilder result = {} # OAuthException, code: 100, error_subcode: 2018218, message: (#100) No profile available for this user # We don't need to capture this error as we don't care about contact params in case of echo messages - ChatwootExceptionTracker.new(e, account: @inbox.account).capture_exception unless @outgoing_echo + if e.message.include?('2018218') + Rails.logger.warn e + else + ChatwootExceptionTracker.new(e, account: @inbox.account).capture_exception unless @outgoing_echo + end rescue StandardError => e result = {} ChatwootExceptionTracker.new(e, account: @inbox.account).capture_exception diff --git a/app/builders/v2/report_builder.rb b/app/builders/v2/report_builder.rb index 5cd8b4a63..b786ba25c 100644 --- a/app/builders/v2/report_builder.rb +++ b/app/builders/v2/report_builder.rb @@ -74,7 +74,7 @@ class V2::ReportBuilder :created_at, default_value: 0, range: range, - permit: %w[day week month year], + permit: %w[day week month year hour], time_zone: @timezone ) end diff --git a/app/controllers/api/v1/accounts/articles_controller.rb b/app/controllers/api/v1/accounts/articles_controller.rb index b6e07be5f..f1689d918 100644 --- a/app/controllers/api/v1/accounts/articles_controller.rb +++ b/app/controllers/api/v1/accounts/articles_controller.rb @@ -55,9 +55,9 @@ class Api::V1::Accounts::ArticlesController < Api::V1::Accounts::BaseController def article_params params.require(:article).permit( - :title, :slug, :content, :description, :position, :category_id, :author_id, :associated_article_id, :status, meta: [:title, - :description, - { tags: [] }] + :title, :slug, :position, :content, :description, :position, :category_id, :author_id, :associated_article_id, :status, meta: [:title, + :description, + { tags: [] }] ) end diff --git a/app/controllers/api/v1/accounts/conversations_controller.rb b/app/controllers/api/v1/accounts/conversations_controller.rb index ec107dfff..10a676738 100644 --- a/app/controllers/api/v1/accounts/conversations_controller.rb +++ b/app/controllers/api/v1/accounts/conversations_controller.rb @@ -142,6 +142,8 @@ class Api::V1::Accounts::ConversationsController < Api::V1::Accounts::BaseContro # and deprecate the support of passing only source_id as the param @contact_inbox ||= ::ContactInbox.find_by!(source_id: params[:source_id]) authorize @contact_inbox.inbox, :show? + rescue ActiveRecord::RecordNotUnique + render json: { error: 'source_id should be unique' }, status: :unprocessable_entity end def build_contact_inbox diff --git a/app/controllers/api/v1/accounts/integrations/apps_controller.rb b/app/controllers/api/v1/accounts/integrations/apps_controller.rb index 7ec5cc5ec..358a15d87 100644 --- a/app/controllers/api/v1/accounts/integrations/apps_controller.rb +++ b/app/controllers/api/v1/accounts/integrations/apps_controller.rb @@ -1,5 +1,5 @@ class Api::V1::Accounts::Integrations::AppsController < Api::V1::Accounts::BaseController - before_action :check_admin_authorization? + before_action :check_admin_authorization?, except: [:index, :show] before_action :fetch_apps, only: [:index] before_action :fetch_app, only: [:show] diff --git a/app/controllers/api/v1/accounts/search_controller.rb b/app/controllers/api/v1/accounts/search_controller.rb new file mode 100644 index 000000000..35979f70f --- /dev/null +++ b/app/controllers/api/v1/accounts/search_controller.rb @@ -0,0 +1,28 @@ +class Api::V1::Accounts::SearchController < Api::V1::Accounts::BaseController + def index + @result = search('all') + end + + def conversations + @result = search('Conversation') + end + + def contacts + @result = search('Contact') + end + + def messages + @result = search('Message') + end + + private + + def search(search_type) + SearchService.new( + current_user: Current.user, + current_account: Current.account, + search_type: search_type, + params: params + ).perform + end +end diff --git a/app/controllers/api/v1/widget/messages_controller.rb b/app/controllers/api/v1/widget/messages_controller.rb index 7ff77be32..6287c94ee 100644 --- a/app/controllers/api/v1/widget/messages_controller.rb +++ b/app/controllers/api/v1/widget/messages_controller.rb @@ -17,7 +17,7 @@ class Api::V1::Widget::MessagesController < Api::V1::Widget::BaseController @message.update!(submitted_email: contact_email) ContactIdentifyAction.new( contact: @contact, - params: { email: contact_email } + params: { email: contact_email, name: contact_name } ).perform else @message.update!(message_update_params[:message]) diff --git a/app/controllers/microsoft/callbacks_controller.rb b/app/controllers/microsoft/callbacks_controller.rb index 5b765f001..c3ecb8c09 100644 --- a/app/controllers/microsoft/callbacks_controller.rb +++ b/app/controllers/microsoft/callbacks_controller.rb @@ -45,13 +45,18 @@ class Microsoft::CallbacksController < ApplicationController channel_email.inbox end + # Fallback name, for when name field is missing from users_data + def fallback_name + users_data['email'].split('@').first.parameterize.titleize + end + def create_microsoft_channel_with_inbox ActiveRecord::Base.transaction do channel_email = Channel::Email.create!(email: users_data['email'], account: account) account.inboxes.create!( account: account, channel: channel_email, - name: users_data['name'] + name: users_data['name'] || fallback_name ) channel_email end diff --git a/app/controllers/public/api/v1/inboxes/messages_controller.rb b/app/controllers/public/api/v1/inboxes/messages_controller.rb index 925c16d38..4dc780fff 100644 --- a/app/controllers/public/api/v1/inboxes/messages_controller.rb +++ b/app/controllers/public/api/v1/inboxes/messages_controller.rb @@ -12,6 +12,8 @@ class Public::Api::V1::Inboxes::MessagesController < Public::Api::V1::InboxesCon end def update + render json: { error: 'You cannot update the CSAT survey after 14 days' }, status: :unprocessable_entity and return if check_csat_locked + @message.update!(message_update_params) rescue StandardError => e render json: { error: @contact.errors, message: e.message }.to_json, status: :internal_server_error @@ -43,7 +45,7 @@ class Public::Api::V1::Inboxes::MessagesController < Public::Api::V1::InboxesCon end def message_update_params - params.permit(submitted_values: [:name, :title, :value]) + params.permit(submitted_values: [:name, :title, :value, { csat_survey_response: [:feedback_message, :rating] }]) end def permitted_params @@ -64,4 +66,8 @@ class Public::Api::V1::Inboxes::MessagesController < Public::Api::V1::InboxesCon message_type: :incoming } end + + def check_csat_locked + (Time.zone.now.to_date - @message.created_at.to_date).to_i > 14 and @message.content_type == 'input_csat' + end end diff --git a/app/controllers/public/api/v1/portals/articles_controller.rb b/app/controllers/public/api/v1/portals/articles_controller.rb index 22ffbbf7a..af5410fc3 100644 --- a/app/controllers/public/api/v1/portals/articles_controller.rb +++ b/app/controllers/public/api/v1/portals/articles_controller.rb @@ -1,4 +1,4 @@ -class Public::Api::V1::Portals::ArticlesController < PublicController +class Public::Api::V1::Portals::ArticlesController < Public::Api::V1::Portals::BaseController before_action :ensure_custom_domain_request, only: [:show, :index] before_action :portal before_action :set_category, except: [:index] @@ -8,6 +8,7 @@ class Public::Api::V1::Portals::ArticlesController < PublicController def index @articles = @portal.articles @articles = @articles.search(list_params) if list_params.present? + @articles.order(position: :asc) end def show; end @@ -15,21 +16,30 @@ class Public::Api::V1::Portals::ArticlesController < PublicController private def set_article - @article = @category.articles.find(params[:id]) + @article = @category.articles.find(permitted_params[:id]) @article.increment_view_count @parsed_content = render_article_content(@article.content) end def set_category - @category = @portal.categories.find_by!(slug: params[:category_slug]) if params[:category_slug].present? + return if permitted_params[:category_slug].blank? + + @category = @portal.categories.find_by!( + slug: permitted_params[:category_slug], + locale: permitted_params[:locale] + ) end def portal - @portal ||= Portal.find_by!(slug: params[:slug], archived: false) + @portal ||= Portal.find_by!(slug: permitted_params[:slug], archived: false) end def list_params - params.permit(:query) + params.permit(:query, :locale) + end + + def permitted_params + params.permit(:slug, :category_slug, :locale, :id) end def render_article_content(content) diff --git a/app/controllers/public/api/v1/portals/base_controller.rb b/app/controllers/public/api/v1/portals/base_controller.rb new file mode 100644 index 000000000..35dbc3ff9 --- /dev/null +++ b/app/controllers/public/api/v1/portals/base_controller.rb @@ -0,0 +1,22 @@ +class Public::Api::V1::Portals::BaseController < PublicController + around_action :set_locale + + private + + def set_locale(&) + switch_locale_with_portal(&) if params[:locale].present? + end + + def switch_locale_with_portal(&) + locale_without_variant = params[:locale].split('_')[0] + is_locale_available = I18n.available_locales.map(&:to_s).include?(params[:locale]) + is_locale_variant_available = I18n.available_locales.map(&:to_s).include?(locale_without_variant) + if is_locale_available + @locale = params[:locale] + elsif is_locale_variant_available + @locale = locale_without_variant + end + + I18n.with_locale(@locale, &) + end +end diff --git a/app/controllers/public/api/v1/portals/categories_controller.rb b/app/controllers/public/api/v1/portals/categories_controller.rb index 6a6ba6377..7326f383b 100644 --- a/app/controllers/public/api/v1/portals/categories_controller.rb +++ b/app/controllers/public/api/v1/portals/categories_controller.rb @@ -1,11 +1,11 @@ -class Public::Api::V1::Portals::CategoriesController < PublicController +class Public::Api::V1::Portals::CategoriesController < Public::Api::V1::Portals::BaseController before_action :ensure_custom_domain_request, only: [:show, :index] before_action :portal before_action :set_category, only: [:show] layout 'portal' def index - @categories = @portal.categories + @categories = @portal.categories.order(position: :asc) end def show; end diff --git a/app/controllers/public/api/v1/portals_controller.rb b/app/controllers/public/api/v1/portals_controller.rb index da9b23956..fe51ed161 100644 --- a/app/controllers/public/api/v1/portals_controller.rb +++ b/app/controllers/public/api/v1/portals_controller.rb @@ -1,4 +1,4 @@ -class Public::Api::V1::PortalsController < PublicController +class Public::Api::V1::PortalsController < Public::Api::V1::Portals::BaseController before_action :ensure_custom_domain_request, only: [:show] before_action :portal before_action :redirect_to_portal_with_locale, only: [:show] diff --git a/app/controllers/widgets_controller.rb b/app/controllers/widgets_controller.rb index 9639b28b2..a4e40eb9c 100644 --- a/app/controllers/widgets_controller.rb +++ b/app/controllers/widgets_controller.rb @@ -5,6 +5,7 @@ class WidgetsController < ActionController::Base before_action :set_global_config before_action :set_web_widget before_action :ensure_account_is_active + before_action :ensure_location_is_supported before_action :set_token before_action :set_contact before_action :build_contact @@ -18,6 +19,9 @@ class WidgetsController < ActionController::Base def set_web_widget @web_widget = ::Channel::WebWidget.find_by!(website_token: permitted_params[:website_token]) + rescue ActiveRecord::RecordNotFound + Rails.logger.error('web widget does not exist') + render json: { error: 'web widget does not exist' }, status: :not_found end def set_token @@ -51,6 +55,8 @@ class WidgetsController < ActionController::Base render json: { error: 'Account is suspended' }, status: :unauthorized unless @web_widget.inbox.account.active? end + def ensure_location_is_supported; end + def additional_attributes if @web_widget.inbox.account.feature_enabled?('ip_lookup') { created_at_ip: request.remote_ip } @@ -67,3 +73,5 @@ class WidgetsController < ActionController::Base response.headers.delete('X-Frame-Options') end end + +WidgetsController.prepend_mod_with('WidgetsController') diff --git a/app/dashboards/access_token_dashboard.rb b/app/dashboards/access_token_dashboard.rb index 8d4f7840e..d3f05a799 100644 --- a/app/dashboards/access_token_dashboard.rb +++ b/app/dashboards/access_token_dashboard.rb @@ -10,7 +10,7 @@ class AccessTokenDashboard < Administrate::BaseDashboard ATTRIBUTE_TYPES = { owner: Field::Polymorphic, id: Field::Number, - token: Field::String, + token: SecretField, created_at: Field::DateTime, updated_at: Field::DateTime }.freeze diff --git a/app/fields/secret_field.rb b/app/fields/secret_field.rb new file mode 100644 index 000000000..6bc7c535a --- /dev/null +++ b/app/fields/secret_field.rb @@ -0,0 +1,4 @@ +require 'administrate/field/base' + +class SecretField < Administrate::Field::String +end diff --git a/app/finders/email_channel_finder.rb b/app/finders/email_channel_finder.rb index 1c8eaeaf2..3fb7edd53 100644 --- a/app/finders/email_channel_finder.rb +++ b/app/finders/email_channel_finder.rb @@ -1,4 +1,6 @@ class EmailChannelFinder + include EmailHelper + def initialize(email_object) @email_object = email_object end @@ -7,7 +9,8 @@ class EmailChannelFinder channel = nil recipient_mails = @email_object.to.to_a + @email_object.cc.to_a recipient_mails.each do |email| - channel = Channel::Email.find_by('lower(email) = ? OR lower(forward_to_email) = ?', email.downcase, email.downcase) + normalized_email = normalize_email_with_plus_addressing(email) + channel = Channel::Email.find_by('lower(email) = ? OR lower(forward_to_email) = ?', normalized_email, normalized_email) break if channel.present? end channel diff --git a/app/helpers/email_helper.rb b/app/helpers/email_helper.rb index 256a50387..46689ba00 100644 --- a/app/helpers/email_helper.rb +++ b/app/helpers/email_helper.rb @@ -3,4 +3,11 @@ module EmailHelper domain = email.split('@').last domain.split('.').first end + + # ref: https://www.rfc-editor.org/rfc/rfc5233.html + # This is not a mandatory requirement for email addresses, but it is a common practice. + # john+test@xyc.com is the same as john@xyc.com + def normalize_email_with_plus_addressing(email) + "#{email.split('@').first.split('+').first}@#{email.split('@').last}".downcase + end end diff --git a/app/helpers/reporting_event_helper.rb b/app/helpers/reporting_event_helper.rb index eee1af283..e08b5691c 100644 --- a/app/helpers/reporting_event_helper.rb +++ b/app/helpers/reporting_event_helper.rb @@ -17,6 +17,15 @@ module ReportingEventHelper from_in_inbox_timezone.working_time_until(to_in_inbox_timezone) end + def last_non_human_activity(conversation) + # check if a handoff event already exists + handoff_event = ReportingEvent.where(conversation_id: conversation.id, name: 'conversation_bot_handoff').last + + # if a handoff exists, last non human activity is when the handoff ended, + # otherwise it's when the conversation was created + handoff_event&.event_end_time || conversation.created_at + end + private def configure_working_hours(working_hours) diff --git a/app/javascript/dashboard/App.vue b/app/javascript/dashboard/App.vue index 0ff94efa5..0d0a6906c 100644 --- a/app/javascript/dashboard/App.vue +++ b/app/javascript/dashboard/App.vue @@ -1,5 +1,10 @@ diff --git a/app/javascript/dashboard/components/widgets/InboxName.vue b/app/javascript/dashboard/components/widgets/InboxName.vue index cb0b9fb06..427772cf5 100644 --- a/app/javascript/dashboard/components/widgets/InboxName.vue +++ b/app/javascript/dashboard/components/widgets/InboxName.vue @@ -25,13 +25,15 @@ export default { diff --git a/app/javascript/dashboard/components/widgets/conversation/ChatFilter.vue b/app/javascript/dashboard/components/widgets/conversation/ChatFilter.vue index 4b41f9a50..170d5f661 100644 --- a/app/javascript/dashboard/components/widgets/conversation/ChatFilter.vue +++ b/app/javascript/dashboard/components/widgets/conversation/ChatFilter.vue @@ -44,3 +44,14 @@ export default { }, }; + diff --git a/app/javascript/dashboard/components/widgets/conversation/ConversationBox.vue b/app/javascript/dashboard/components/widgets/conversation/ConversationBox.vue index 0ed8c84d5..c4e4a80c4 100644 --- a/app/javascript/dashboard/components/widgets/conversation/ConversationBox.vue +++ b/app/javascript/dashboard/components/widgets/conversation/ConversationBox.vue @@ -1,7 +1,7 @@