diff --git a/.env.example b/.env.example index efee18735..a97679f7e 100644 --- a/.env.example +++ b/.env.example @@ -199,7 +199,7 @@ ANDROID_SHA256_CERT_FINGERPRINT=AC:73:8E:DE:EB:56:EA:CC:10:87:02:A7:65:37:7B:38: ## Rack Attack configuration ## To prevent and throttle abusive requests # ENABLE_RACK_ATTACK=true -# RACK_ATTACK_IP_LIMIT=3000 +# RACK_ATTACK_LIMIT=300 # ENABLE_RACK_ATTACK_WIDGET_API=true ## Running chatwoot as an API only server diff --git a/.eslintrc.js b/.eslintrc.js index 0fae1bc44..a58c77e98 100644 --- a/.eslintrc.js +++ b/.eslintrc.js @@ -6,7 +6,7 @@ module.exports = { 'plugin:storybook/recommended', ], parserOptions: { - parser: 'babel-eslint', + parser: '@babel/eslint-parser', ecmaVersion: 2020, sourceType: 'module', }, @@ -24,13 +24,16 @@ module.exports = { 'jsx-a11y/label-has-for': 'off', 'jsx-a11y/anchor-is-valid': 'off', 'import/no-unresolved': 'off', + 'vue/html-indent': 'off', + 'vue/multi-word-component-names': 'off', 'vue/max-attributes-per-line': [ 'error', { - singleline: 20, + singleline: { + max: 20, + }, multiline: { max: 1, - allowFirstLine: false, }, }, ], @@ -47,6 +50,7 @@ module.exports = { }, ], 'vue/no-v-html': 'off', + 'vue/component-definition-name-casing': 'off', 'vue/singleline-html-element-content-newline': 'off', 'import/extensions': ['off'], 'no-console': 'error', diff --git a/.github/workflows/run_response_bot_spec.yml b/.github/workflows/run_response_bot_spec.yml index ded5c903e..6fd6a7b22 100644 --- a/.github/workflows/run_response_bot_spec.yml +++ b/.github/workflows/run_response_bot_spec.yml @@ -69,7 +69,12 @@ jobs: # Run Response Bot specs - name: Run backend tests run: | - bundle exec rspec spec/enterprise/controllers/api/v1/accounts/response_sources_controller_spec.rb spec/enterprise/controllers/enterprise/api/v1/accounts/inboxes_controller_spec.rb:47 --profile=10 --format documentation + bundle exec rspec \ + spec/enterprise/controllers/api/v1/accounts/response_sources_controller_spec.rb \ + spec/enterprise/services/enterprise/message_templates/response_bot_service_spec.rb \ + spec/enterprise/controllers/enterprise/api/v1/accounts/inboxes_controller_spec.rb:47 \ + --profile=10 \ + --format documentation - name: Upload rails log folder uses: actions/upload-artifact@v3 diff --git a/.rubocop.yml b/.rubocop.yml index 22e59629d..484e424ed 100644 --- a/.rubocop.yml +++ b/.rubocop.yml @@ -58,6 +58,7 @@ Metrics/BlockLength: Metrics/ModuleLength: Exclude: - lib/seeders/message_seeder.rb + - spec/support/slack_stubs.rb Rails/ApplicationController: Exclude: - 'app/controllers/api/v1/widget/messages_controller.rb' diff --git a/Gemfile b/Gemfile index 8a54e1751..d68bbd8de 100644 --- a/Gemfile +++ b/Gemfile @@ -15,7 +15,7 @@ gem 'browser' gem 'hashie' gem 'jbuilder' gem 'kaminari' -gem 'responders' +gem 'responders', '>= 3.1.1' gem 'rest-client' gem 'telephone_number' gem 'time_diff' @@ -67,7 +67,7 @@ gem 'webpacker' gem 'barnes' ##--- gems for authentication & authorization ---## -gem 'devise' +gem 'devise', '>= 4.9.3' gem 'devise-secure_password', git: 'https://github.com/chatwoot/devise-secure_password', branch: 'chatwoot' gem 'devise_token_auth' # authorization @@ -92,7 +92,7 @@ gem 'twitty', '~> 0.1.5' # facebook client gem 'koala' # slack client -gem 'slack-ruby-client', '~> 2.0.0' +gem 'slack-ruby-client', '~> 2.2.0' # for dialogflow integrations gem 'google-cloud-dialogflow-v2' gem 'grpc' @@ -109,9 +109,9 @@ gem 'elastic-apm', require: false gem 'newrelic_rpm', require: false gem 'newrelic-sidekiq-metrics', require: false gem 'scout_apm', require: false -gem 'sentry-rails', '>= 5.11.0', require: false +gem 'sentry-rails', '>= 5.12.0', require: false gem 'sentry-ruby', require: false -gem 'sentry-sidekiq', '>= 5.11.0', require: false +gem 'sentry-sidekiq', '>= 5.12.0', require: false ##-- background job processing --## gem 'sidekiq', '>= 7.1.3' @@ -154,12 +154,12 @@ gem 'stripe' gem 'faker' # Include logrange conditionally in intializer using env variable -gem 'lograge', '~> 0.13.0', require: false +gem 'lograge', '~> 0.14.0', require: false # worked with microsoft refresh token gem 'omniauth-oauth2' -gem 'audited', '~> 5.3' +gem 'audited', '~> 5.4', '>= 5.4.0' # need for google auth gem 'omniauth' diff --git a/Gemfile.lock b/Gemfile.lock index 264f65389..a47f351c4 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -126,8 +126,8 @@ GEM rake (>= 10.4, < 14.0) ast (2.4.2) attr_extras (7.1.0) - audited (5.3.3) - activerecord (>= 5.0, < 7.1) + audited (5.4.0) + activerecord (>= 5.0, < 7.2) request_store (~> 1.2) aws-eventstream (1.2.0) aws-partitions (1.760.0) @@ -148,7 +148,7 @@ GEM barnes (0.0.9) multi_json (~> 1) statsd-ruby (~> 1.1) - bcrypt (3.1.18) + bcrypt (3.1.19) bindex (0.8.1) blingfire (0.1.8) bootsnap (1.16.0) @@ -193,7 +193,7 @@ GEM irb (>= 1.5.0) reline (>= 0.3.1) declarative (0.0.20) - devise (4.9.2) + devise (4.9.3) bcrypt (~> 3.0) orm_adapter (~> 0.1) railties (>= 4.1.0) @@ -273,7 +273,7 @@ GEM googleauth (~> 1.0) grpc (~> 1.36) geocoder (1.8.1) - gli (2.21.0) + gli (2.21.1) globalid (1.2.1) activesupport (>= 6.1) gmail_xoauth (0.4.2) @@ -434,12 +434,12 @@ GEM llhttp-ffi (0.4.0) ffi-compiler (~> 1.0) rake (~> 13.0) - lograge (0.13.0) + lograge (0.14.0) actionpack (>= 4) activesupport (>= 4) railties (>= 4) request_store (~> 1.0) - loofah (2.21.3) + loofah (2.21.4) crass (~> 1.0.2) nokogiri (>= 1.12.0) mail (2.8.1) @@ -616,7 +616,7 @@ GEM uber (< 0.2.0) request_store (1.5.1) rack (>= 1.4) - responders (3.1.0) + responders (3.1.1) actionpack (>= 5.2) railties (>= 5.2) rest-client (2.1.0) @@ -702,18 +702,18 @@ GEM activesupport (>= 4) selectize-rails (0.12.6) semantic_range (3.0.0) - sentry-rails (5.11.0) + sentry-rails (5.12.0) railties (>= 5.0) - sentry-ruby (~> 5.11.0) - sentry-ruby (5.11.0) + sentry-ruby (~> 5.12.0) + sentry-ruby (5.12.0) concurrent-ruby (~> 1.0, >= 1.0.2) - sentry-sidekiq (5.11.0) - sentry-ruby (~> 5.11.0) + sentry-sidekiq (5.12.0) + sentry-ruby (~> 5.12.0) sidekiq (>= 3.0) sexp_processor (4.17.0) shoulda-matchers (5.3.0) activesupport (>= 5.2.0) - sidekiq (7.1.3) + sidekiq (7.1.6) concurrent-ruby (< 2) connection_pool (>= 2.3.0) rack (>= 2.2.4) @@ -732,13 +732,12 @@ GEM json (>= 1.8, < 3) simplecov-html (~> 0.10.0) simplecov-html (0.10.2) - slack-ruby-client (2.0.0) + slack-ruby-client (2.2.0) faraday (>= 2.0) faraday-mashify faraday-multipart gli hashie - websocket-driver snaky_hash (2.0.1) hashie version_gem (~> 1.1, >= 1.1.1) @@ -817,7 +816,7 @@ GEM working_hours (1.4.1) activesupport (>= 3.2) tzinfo - zeitwerk (2.6.11) + zeitwerk (2.6.12) PLATFORMS arm64-darwin-20 @@ -839,7 +838,7 @@ DEPENDENCIES administrate-field-belongs_to_search annotate attr_extras - audited (~> 5.3) + audited (~> 5.4, >= 5.4.0) aws-sdk-s3 azure-storage-blob! barnes @@ -856,7 +855,7 @@ DEPENDENCIES database_cleaner ddtrace debug (~> 1.8) - devise + devise (>= 4.9.3) devise-secure_password! devise_token_auth dotenv-rails @@ -892,7 +891,7 @@ DEPENDENCIES line-bot-api liquid listen - lograge (~> 0.13.0) + lograge (~> 0.14.0) maxminddb mock_redis neighbor @@ -916,7 +915,7 @@ DEPENDENCIES rails (~> 7.0.8.0) redis redis-namespace - responders + responders (>= 3.1.1) rest-client reverse_markdown rspec-rails @@ -928,14 +927,14 @@ DEPENDENCIES scout_apm scss_lint seed_dump - sentry-rails (>= 5.11.0) + sentry-rails (>= 5.12.0) sentry-ruby - sentry-sidekiq (>= 5.11.0) + sentry-sidekiq (>= 5.12.0) shoulda-matchers sidekiq (>= 7.1.3) sidekiq-cron (>= 1.10.1) simplecov (= 0.17.1) - slack-ruby-client (~> 2.0.0) + slack-ruby-client (~> 2.2.0) spring spring-watcher-listen squasher diff --git a/VERSION_CWCTL b/VERSION_CWCTL index 197c4d5c2..e70b4523a 100644 --- a/VERSION_CWCTL +++ b/VERSION_CWCTL @@ -1 +1 @@ -2.4.0 +2.6.0 diff --git a/app/assets/stylesheets/administrate/base/_forms.scss b/app/assets/stylesheets/administrate/base/_forms.scss index bf014a746..dcf535b58 100644 --- a/app/assets/stylesheets/administrate/base/_forms.scss +++ b/app/assets/stylesheets/administrate/base/_forms.scss @@ -24,14 +24,6 @@ select { font-size: $base-font-size; } -input, -select, -textarea { - display: block; - font-family: $base-font-family; - font-size: 16px; -} - [type="color"], [type="date"], [type="datetime-local"], @@ -51,6 +43,7 @@ textarea { background-color: $white; border: $base-border; border-radius: $base-border-radius; + font-family: $base-font-family; padding: 0.5em; transition: border-color $base-duration $base-timing; width: 100%; diff --git a/app/assets/stylesheets/administrate/components/_attributes.scss b/app/assets/stylesheets/administrate/components/_attributes.scss index 713d9f523..2b2936650 100644 --- a/app/assets/stylesheets/administrate/components/_attributes.scss +++ b/app/assets/stylesheets/administrate/components/_attributes.scss @@ -4,8 +4,8 @@ float: left; margin-bottom: $base-spacing; margin-top: 0.25em; - text-align: right; - width: calc(15% - 1rem); + text-align: left; + width: calc(16% - 1rem); } .preserve-whitespace { @@ -17,7 +17,7 @@ float: left; margin-bottom: $base-spacing; margin-left: 2rem; - width: calc(85% - 1rem); + width: calc(84% - 1rem); } .attribute--nested { diff --git a/app/assets/stylesheets/administrate/components/_buttons.scss b/app/assets/stylesheets/administrate/components/_buttons.scss index 3e021e658..7b2f62045 100644 --- a/app/assets/stylesheets/administrate/components/_buttons.scss +++ b/app/assets/stylesheets/administrate/components/_buttons.scss @@ -10,7 +10,7 @@ input[type="submit"], color: $white; cursor: pointer; display: inline-block; - font-size: $font-size-default; + font-size: $font-size-small; -webkit-font-smoothing: antialiased; font-weight: $font-weight-medium; line-height: 1; diff --git a/app/assets/stylesheets/administrate/components/_main-content.scss b/app/assets/stylesheets/administrate/components/_main-content.scss index d03229828..590bb0985 100644 --- a/app/assets/stylesheets/administrate/components/_main-content.scss +++ b/app/assets/stylesheets/administrate/components/_main-content.scss @@ -1,13 +1,18 @@ .main-content { font-size: $font-size-default; - left: 23rem; + left: 21rem; position: absolute; right: 0; top: 0; } .main-content__body { + font-size: $font-size-small; padding: $space-two; + + table { + font-size: $font-size-small; + } } .main-content__header { @@ -20,7 +25,7 @@ } .main-content__page-title { - font-size: $font-size-large; + font-size: $font-size-medium; font-weight: $font-weight-medium; margin-right: auto; } diff --git a/app/assets/stylesheets/administrate/components/_navigation.scss b/app/assets/stylesheets/administrate/components/_navigation.scss index 1e7e35d25..4ffc42190 100644 --- a/app/assets/stylesheets/administrate/components/_navigation.scss +++ b/app/assets/stylesheets/administrate/components/_navigation.scss @@ -1,7 +1,12 @@ .logo-brand { margin-bottom: $space-normal; padding: $space-normal $space-smaller $space-small; - text-align: center; + text-align: left; + + img { + margin-bottom: $space-smaller; + max-height: 3rem; + } } .navigation { @@ -19,12 +24,13 @@ padding: $space-normal; position: fixed; top: 0; - width: 23rem; + width: 21rem; z-index: 1023; li { align-items: center; display: flex; + font-size: $font-size-small; a { color: $color-gray; @@ -35,6 +41,10 @@ min-width: $space-medium; } } + + hr { + margin: $space-slab; + } } .navigation__link { @@ -43,7 +53,7 @@ display: block; line-height: 1; margin-bottom: $space-smaller; - padding: $space-one; + padding: $space-small; &:hover { color: $blue; diff --git a/app/assets/stylesheets/administrate/library/_variables.scss b/app/assets/stylesheets/administrate/library/_variables.scss index 5bb3c0710..086428556 100644 --- a/app/assets/stylesheets/administrate/library/_variables.scss +++ b/app/assets/stylesheets/administrate/library/_variables.scss @@ -4,7 +4,7 @@ $base-font-family: PlusJakarta, Inter, -apple-system, BlinkMacSystemFont, "Segoe sans-serif !default; $heading-font-family: $base-font-family !default; -$base-font-size: 16px !default; +$base-font-size: 14px !default; $base-line-height: 1.5 !default; $heading-line-height: 1.2 !default; diff --git a/app/builders/messages/facebook/message_builder.rb b/app/builders/messages/facebook/message_builder.rb index e0343c0a0..3efb184b9 100644 --- a/app/builders/messages/facebook/message_builder.rb +++ b/app/builders/messages/facebook/message_builder.rb @@ -93,6 +93,9 @@ class Messages::Facebook::MessageBuilder < Messages::Messenger::MessageBuilder message_type: @message_type, content: response.content, source_id: response.identifier, + content_attributes: { + in_reply_to_external_id: response.in_reply_to_external_id + }, sender: @outgoing_echo ? nil : @contact_inbox.contact } end diff --git a/app/builders/v2/report_builder.rb b/app/builders/v2/report_builder.rb index c96950ee9..1442c5799 100644 --- a/app/builders/v2/report_builder.rb +++ b/app/builders/v2/report_builder.rb @@ -15,7 +15,10 @@ class V2::ReportBuilder end def timeseries - send(params[:metric]) + return send(params[:metric]) if metric_valid? + + Rails.logger.error "ReportBuilder: Invalid metric - #{params[:metric]}" + {} end # For backward compatible with old report @@ -53,6 +56,16 @@ class V2::ReportBuilder private + def metric_valid? + %w[conversations_count + incoming_messages_count + outgoing_messages_count + avg_first_response_time + avg_resolution_time reply_time + resolutions_count + reply_time].include?(params[:metric]) + end + def inbox @inbox ||= account.inboxes.find(params[:id]) end diff --git a/app/controllers/api/v1/accounts/actions/contact_merges_controller.rb b/app/controllers/api/v1/accounts/actions/contact_merges_controller.rb index 9daee7b04..ce38fd9ab 100644 --- a/app/controllers/api/v1/accounts/actions/contact_merges_controller.rb +++ b/app/controllers/api/v1/accounts/actions/contact_merges_controller.rb @@ -9,7 +9,6 @@ class Api::V1::Accounts::Actions::ContactMergesController < Api::V1::Accounts::B mergee_contact: @mergee_contact ) contact_merge_action.perform - render json: @base_contact end private diff --git a/app/controllers/api/v1/accounts/contact_inboxes_controller.rb b/app/controllers/api/v1/accounts/contact_inboxes_controller.rb new file mode 100644 index 000000000..ed7066895 --- /dev/null +++ b/app/controllers/api/v1/accounts/contact_inboxes_controller.rb @@ -0,0 +1,21 @@ +class Api::V1::Accounts::ContactInboxesController < Api::V1::Accounts::BaseController + before_action :ensure_inbox + + def filter + contact_inbox = @inbox.contact_inboxes.where(inbox_id: permitted_params[:inbox_id], source_id: permitted_params[:source_id]) + return head :not_found if contact_inbox.empty? + + @contact = contact_inbox.first.contact + end + + private + + def ensure_inbox + @inbox = Current.account.inboxes.find(permitted_params[:inbox_id]) + authorize @inbox, :show? + end + + def permitted_params + params.permit(:inbox_id, :source_id) + end +end diff --git a/app/controllers/api/v1/accounts/conversations_controller.rb b/app/controllers/api/v1/accounts/conversations_controller.rb index cd8547213..eb3187d54 100644 --- a/app/controllers/api/v1/accounts/conversations_controller.rb +++ b/app/controllers/api/v1/accounts/conversations_controller.rb @@ -60,7 +60,7 @@ class Api::V1::Accounts::ConversationsController < Api::V1::Accounts::BaseContro end def toggle_status - if params[:status] + if params[:status].present? set_conversation_status @status = @conversation.save! else diff --git a/app/controllers/api/v1/integrations/webhooks_controller.rb b/app/controllers/api/v1/integrations/webhooks_controller.rb index 94373c92e..283be3d77 100644 --- a/app/controllers/api/v1/integrations/webhooks_controller.rb +++ b/app/controllers/api/v1/integrations/webhooks_controller.rb @@ -1,7 +1,15 @@ class Api::V1::Integrations::WebhooksController < ApplicationController def create - builder = Integrations::Slack::IncomingMessageBuilder.new(params) + builder = Integrations::Slack::IncomingMessageBuilder.new(permitted_params) response = builder.perform render json: response end + + private + + # TODO: This is a temporary solution to permit all params for slack unfurling job. + # We should only permit the params that we use. Handle all the params based on events and send it to the respective services. + def permitted_params + params.permit! + end end diff --git a/app/controllers/api/v1/widget/conversations_controller.rb b/app/controllers/api/v1/widget/conversations_controller.rb index 2c72a0361..df0ef64ff 100644 --- a/app/controllers/api/v1/widget/conversations_controller.rb +++ b/app/controllers/api/v1/widget/conversations_controller.rb @@ -1,5 +1,6 @@ class Api::V1::Widget::ConversationsController < Api::V1::Widget::BaseController include Events::Types + before_action :render_not_found_if_empty, only: [:toggle_typing, :toggle_status, :set_custom_attributes, :destroy_custom_attributes] def index @conversation = conversation @@ -27,6 +28,7 @@ class Api::V1::Widget::ConversationsController < Api::V1::Widget::BaseController conversation.contact_last_seen_at = DateTime.now.utc conversation.save! + ::Conversations::MarkMessagesAsReadJob.perform_later(conversation) head :ok end @@ -41,8 +43,6 @@ class Api::V1::Widget::ConversationsController < Api::V1::Widget::BaseController end def toggle_typing - head :ok && return if conversation.nil? - case permitted_params[:typing_status] when 'on' trigger_typing_event(CONVERSATION_TYPING_ON) @@ -54,8 +54,6 @@ class Api::V1::Widget::ConversationsController < Api::V1::Widget::BaseController end def toggle_status - return head :not_found if conversation.nil? - return head :forbidden unless @web_widget.end_conversation? unless conversation.resolved? @@ -81,6 +79,10 @@ class Api::V1::Widget::ConversationsController < Api::V1::Widget::BaseController Rails.configuration.dispatcher.dispatch(event, Time.zone.now, conversation: conversation, user: @contact) end + def render_not_found_if_empty + return head :not_found if conversation.nil? + end + def permitted_params params.permit(:id, :typing_status, :website_token, :email, contact: [:name, :email, :phone_number], message: [:content, :referer_url, :timestamp, :echo_id], diff --git a/app/controllers/concerns/request_exception_handler.rb b/app/controllers/concerns/request_exception_handler.rb index 2f53fdc2b..ccab0090a 100644 --- a/app/controllers/concerns/request_exception_handler.rb +++ b/app/controllers/concerns/request_exception_handler.rb @@ -9,11 +9,14 @@ module RequestExceptionHandler def handle_with_exception yield - rescue ActiveRecord::RecordNotFound + rescue ActiveRecord::RecordNotFound => e + log_handled_error(e) render_not_found_error('Resource could not be found') - rescue Pundit::NotAuthorizedError + rescue Pundit::NotAuthorizedError => e + log_handled_error(e) render_unauthorized('You are not authorized to do this action') rescue ActionController::ParameterMissing => e + log_handled_error(e) render_could_not_create_error(e.message) ensure # to address the thread variable leak issues in Puma/Thin webserver @@ -41,6 +44,7 @@ module RequestExceptionHandler end def render_record_invalid(exception) + log_handled_error(exception) render json: { message: exception.record.errors.full_messages.join(', '), attributes: exception.record.errors.attribute_names @@ -48,6 +52,11 @@ module RequestExceptionHandler end def render_error_response(exception) + log_handled_error(exception) render json: exception.to_hash, status: exception.http_status end + + def log_handled_error(exception) + logger.info("Handled error: #{exception.inspect}") + end end diff --git a/app/controllers/public/api/v1/inboxes/conversations_controller.rb b/app/controllers/public/api/v1/inboxes/conversations_controller.rb index 3d86ca87c..c79952372 100644 --- a/app/controllers/public/api/v1/inboxes/conversations_controller.rb +++ b/app/controllers/public/api/v1/inboxes/conversations_controller.rb @@ -33,7 +33,7 @@ class Public::Api::V1::Inboxes::ConversationsController < Public::Api::V1::Inbox end def create_conversation - ::Conversation.create!(conversation_params) + ConversationBuilder.new(params: conversation_params, contact_inbox: @contact_inbox).perform end def trigger_typing_event(event) @@ -41,11 +41,6 @@ class Public::Api::V1::Inboxes::ConversationsController < Public::Api::V1::Inbox end def conversation_params - { - account_id: @contact_inbox.contact.account_id, - inbox_id: @contact_inbox.inbox_id, - contact_id: @contact_inbox.contact_id, - contact_inbox_id: @contact_inbox.id - } + params.permit(custom_attributes: {}) end end diff --git a/app/controllers/public/api/v1/portals/base_controller.rb b/app/controllers/public/api/v1/portals/base_controller.rb index 68619baf8..d9fec27b1 100644 --- a/app/controllers/public/api/v1/portals/base_controller.rb +++ b/app/controllers/public/api/v1/portals/base_controller.rb @@ -1,5 +1,6 @@ class Public::Api::V1::Portals::BaseController < PublicController before_action :show_plain_layout + before_action :set_color_scheme around_action :set_locale after_action :allow_iframe_requests @@ -9,6 +10,14 @@ class Public::Api::V1::Portals::BaseController < PublicController @is_plain_layout_enabled = params[:show_plain_layout] == 'true' end + def set_color_scheme + @theme = if %w[dark light].include?(params[:theme]) + params[:theme] + else + '' + end + end + def set_locale(&) switch_locale_with_portal(&) if params[:locale].present? switch_locale_with_article(&) if params[:article_slug].present? diff --git a/app/controllers/slack_uploads_controller.rb b/app/controllers/slack_uploads_controller.rb new file mode 100644 index 000000000..127e77649 --- /dev/null +++ b/app/controllers/slack_uploads_controller.rb @@ -0,0 +1,27 @@ +class SlackUploadsController < ApplicationController + include Rails.application.routes.url_helpers + before_action :set_blob, only: [:show] + + def show + if @blob + redirect_to blob_url + else + redirect_to avatar_url + end + end + + private + + def set_blob + @blob = ActiveStorage::Blob.find_by(key: params[:blob_key]) + end + + def blob_url + url_for(@blob.representation(resize_to_fill: [250, nil])) + end + + def avatar_url + base_url = ENV.fetch('FRONTEND_URL', nil) + "#{base_url}/integrations/slack/#{params[:sender_type]}.png" + end +end diff --git a/app/controllers/twilio/delivery_status_controller.rb b/app/controllers/twilio/delivery_status_controller.rb new file mode 100644 index 000000000..cc7afb0fc --- /dev/null +++ b/app/controllers/twilio/delivery_status_controller.rb @@ -0,0 +1,21 @@ +class Twilio::DeliveryStatusController < ApplicationController + def create + ::Twilio::DeliveryStatusService.new(params: permitted_params).perform + + head :no_content + end + + private + + def permitted_params + params.permit( + :AccountSid, + :From, + :MessageSid, + :MessagingServiceSid, + :MessageStatus, + :ErrorCode, + :ErrorMessage + ) + end +end diff --git a/app/helpers/portal_helper.rb b/app/helpers/portal_helper.rb new file mode 100644 index 000000000..3551b5ac6 --- /dev/null +++ b/app/helpers/portal_helper.rb @@ -0,0 +1,11 @@ +module PortalHelper + def generate_portal_bg_color(portal_color, theme) + base_color = theme == 'dark' ? 'black' : 'white' + "color-mix(in srgb, #{portal_color} 10%, #{base_color})" + end + + def generate_portal_bg(portal_color, theme) + bg_image = theme == 'dark' ? 'grid_dark.svg' : 'grid.svg' + "background: url(/assets/images/hc/#{bg_image}) #{generate_portal_bg_color(portal_color, theme)}" + end +end diff --git a/app/javascript/dashboard/App.vue b/app/javascript/dashboard/App.vue index 649d56d15..6fd470fcf 100644 --- a/app/javascript/dashboard/App.vue +++ b/app/javascript/dashboard/App.vue @@ -1,13 +1,13 @@