diff --git a/.rubocop.yml b/.rubocop.yml index 484e424ed..5add6e26a 100644 --- a/.rubocop.yml +++ b/.rubocop.yml @@ -2,7 +2,6 @@ require: - rubocop-performance - rubocop-rails - rubocop-rspec -inherit_from: .rubocop_todo.yml Layout/LineLength: Max: 150 @@ -12,7 +11,8 @@ Metrics/ClassLength: Exclude: - 'app/models/message.rb' - 'app/models/conversation.rb' - +Metrics/MethodLength: + Max: 19 RSpec/ExampleLength: Max: 25 Style/Documentation: @@ -50,6 +50,7 @@ Lint/OrAssignmentToConstant: Exclude: - 'lib/redis/config.rb' Metrics/BlockLength: + Max: 30 Exclude: - spec/**/* - '**/routes.rb' @@ -102,84 +103,31 @@ RSpec/FactoryBot/SyntaxMethods: Enabled: false Naming/VariableNumber: Enabled: false -Metrics/MethodLength: +Naming/MemoizedInstanceVariableName: Exclude: - - 'db/migrate/20161123131628_devise_token_auth_create_users.rb' - - 'db/migrate/20211219031453_update_foreign_keys_on_delete.rb' -Rails/CreateTableWithTimestamps: - Exclude: - - 'db/migrate/20170207092002_acts_as_taggable_on_migration.acts_as_taggable_on_engine.rb' + - 'app/models/message.rb' Style/GuardClause: Exclude: - 'app/builders/account_builder.rb' - 'app/models/attachment.rb' - 'app/models/message.rb' - - 'db/migrate/20190819005836_add_missing_indexes_on_taggings.acts_as_taggable_on_engine.rb' Metrics/AbcSize: + Max: 26 Exclude: - 'app/controllers/concerns/auth_helper.rb' - - 'db/migrate/20190819005836_add_missing_indexes_on_taggings.acts_as_taggable_on_engine.rb' - - 'db/migrate/20161123131628_devise_token_auth_create_users.rb' - - 'app/controllers/api/v1/accounts/inboxes_controller.rb' - - 'db/migrate/20211219031453_update_foreign_keys_on_delete.rb' -Metrics/CyclomaticComplexity: - Max: 7 - Exclude: - - 'db/migrate/20190819005836_add_missing_indexes_on_taggings.acts_as_taggable_on_engine.rb' -Rails/ReversibleMigration: - Exclude: - - 'db/migrate/20161025070152_removechannelsfrommodels.rb' - - 'db/migrate/20161025070645_remchannel.rb' - - 'db/migrate/20161025070645_remchannel.rb' - - 'db/migrate/20161110102609_removeinboxid.rb' - - 'db/migrate/20170519091539_add_avatar_to_fb.rb' - - 'db/migrate/20191020085608_rename_old_tables.rb' - - 'db/migrate/20191126185833_update_user_invite_foreign_key.rb' - - 'db/migrate/20191130164019_add_template_type_to_messages.rb' - - 'db/migrate/20210513083044_remove_not_null_from_webhook_url_channel_api.rb' -Rails/BulkChangeTable: - Exclude: - - 'db/migrate/20161025070152_removechannelsfrommodels.rb' - - 'db/migrate/20200121190901_create_account_users.rb' - - 'db/migrate/20170211092540_notnullableusers.rb' - - 'db/migrate/20170403095203_contactadder.rb' - - 'db/migrate/20170406104018_add_default_status_conv.rb' - - 'db/migrate/20170511134418_latlong.rb' - - 'db/migrate/20191027054756_create_contact_inboxes.rb' - - 'db/migrate/20191130164019_add_template_type_to_messages.rb' - - 'db/migrate/20210425093724_convert_integration_hook_settings_field.rb' Rails/UniqueValidationWithoutIndex: Exclude: - 'app/models/channel/twitter_profile.rb' - 'app/models/webhook.rb' - 'app/models/contact.rb' - 'app/models/integrations/hook.rb' + - 'app/models/canned_response.rb' + - 'app/models/telegram_bot.rb' Rails/RenderInline: Exclude: - 'app/controllers/swagger_controller.rb' -Performance/CollectionLiteralInLoop: - Exclude: - - 'db/migrate/20210315101919_enable_email_channel.rb' Rails/ThreeStateBooleanColumn: Exclude: - - 'db/migrate/20200509044639_add_hide_input_flag_to_bot_config.rb' - - 'db/migrate/20200605130625_agent_away_message_to_auto_reply.rb' - - 'db/migrate/20200606132552_create_labels.rb' - - 'db/migrate/20201027135006_create_working_hours.rb' - - 'db/migrate/20210112174124_add_hmac_token_to_inbox.rb' - - 'db/migrate/20210114202310_create_teams.rb' - - 'db/migrate/20210212154240_add_request_for_email_on_channel_web_widget.rb' - - 'db/migrate/20210428135041_add_campaigns.rb' - - 'db/migrate/20210602182058_add_hmac_to_api_channel.rb' - - 'db/migrate/20210609133433_add_email_collect_to_inboxes.rb' - - 'db/migrate/20210618095823_add_csat_toggle_for_inbox.rb' - - 'db/migrate/20210927062350_add_trigger_only_during_business_hours_collect_to_campaigns.rb' - - 'db/migrate/20211027073553_add_imap_smtp_config_to_channel_email.rb' - - 'db/migrate/20211109143122_add_tweet_enabled_flag_to_twitter_channel.rb' - - 'db/migrate/20211216110209_add_allow_messages_after_resolved_to_inbox.rb' - - 'db/migrate/20220116103902_add_open_ssl_verify_mode_to_channel_email.rb' - - 'db/migrate/20220216151613_add_open_all_day_to_working_hour.rb' - - 'db/migrate/20220511072655_add_archive_column_to_portal.rb' - 'db/migrate/20230503101201_create_sla_policies.rb' RSpec/IndexedLet: Enabled: false @@ -187,6 +135,8 @@ RSpec/NamedSubject: Enabled: false # we should bring this down +RSpec/MultipleExpectations: + Max: 7 RSpec/MultipleMemoizedHelpers: Max: 14 @@ -203,13 +153,4 @@ AllCops: - 'config/environments/**/*' - 'tmp/**/*' - 'storage/**/*' - - 'db/migrate/20200225162150_init_schema.rb' - - 'db/migrate/20210611180222_create_active_storage_variant_records.active_storage.rb' - - 'db/migrate/20210611180221_add_service_name_to_active_storage_blobs.active_storage.rb' - - db/migrate/20200309213132_add_account_id_to_agent_bot_inboxes.rb - - db/migrate/20200331095710_add_identifier_to_contact.rb - - db/migrate/20200429082655_add_medium_to_twilio_sms.rb - - db/migrate/20200503151130_add_account_feature_flag.rb - - db/migrate/20200927135222_add_last_activity_at_to_conversation.rb - - db/migrate/20210306170117_add_last_activity_at_to_contacts.rb - - db/migrate/20220809104508_revert_cascading_indexes.rb + - 'db/migrate/20230426130150_init_schema.rb' diff --git a/.rubocop_todo.yml b/.rubocop_todo.yml deleted file mode 100644 index 48e714dd6..000000000 --- a/.rubocop_todo.yml +++ /dev/null @@ -1,287 +0,0 @@ -# This configuration was generated by -# `rubocop --auto-gen-config` -# on 2019-10-23 16:47:02 +0530 using RuboCop version 0.73.0. -# The point is for the user to remove these configuration records -# one by one as the offenses are removed from the code base. -# Note that changes in the inspected code, or installation of new -# versions of RuboCop, may require this file to be generated again. - -# Offense count: 1 -Lint/DuplicateMethods: - Exclude: - - 'app/controllers/api/v1/reports_controller.rb' - -# Offense count: 1 -Lint/RescueException: - Exclude: - - 'app/builders/messages/message_builder.rb' - -# Offense count: 4 -Lint/ShadowingOuterLocalVariable: - Exclude: - - 'app/controllers/api/v1/reports_controller.rb' - -# Offense count: 3 -# Configuration parameters: AllowKeywordBlockArguments. -Lint/UnderscorePrefixedVariableName: - Exclude: - - 'app/models/account.rb' - - 'deploy/before_symlink.rb' - -# Offense count: 18 -Lint/UselessAssignment: - Exclude: - - 'app/controllers/api/v1/callbacks_controller.rb' - - 'app/controllers/api/v1/facebook_indicators_controller.rb' - - 'app/listeners/action_cable_listener.rb' - - 'app/listeners/reporting_listener.rb' - - 'app/models/channel/facebook_page.rb' - - 'app/models/facebook_page.rb' - -# Offense count: 14 -Metrics/AbcSize: - Max: 26 - -# Offense count: 1 -# Configuration parameters: CountComments, ExcludedMethods. -# ExcludedMethods: refine -Metrics/BlockLength: - Max: 30 - -# Offense count: 2 -Metrics/CyclomaticComplexity: - Max: 7 - -# Offense count: 10 -# Configuration parameters: CountComments, ExcludedMethods. -Metrics/MethodLength: - Max: 19 - -# Offense count: 1 -Metrics/PerceivedComplexity: - Max: 8 - -# Offense count: 6 -Naming/AccessorMethodName: - Exclude: - - 'app/builders/report_builder.rb' - - 'app/controllers/api/v1/accounts_controller.rb' - - 'app/controllers/api/v1/callbacks_controller.rb' - - 'app/controllers/api/v1/conversations_controller.rb' - -# Offense count: 9 -# Configuration parameters: EnforcedStyleForLeadingUnderscores. -# SupportedStylesForLeadingUnderscores: disallowed, required, optional -Naming/MemoizedInstanceVariableName: - Exclude: - - 'app/controllers/api/base_controller.rb' - - 'app/controllers/api/v1/conversations_controller.rb' - - 'app/controllers/api/v1/webhooks_controller.rb' - - 'app/controllers/application_controller.rb' - - 'app/models/message.rb' - - 'lib/integrations/widget/outgoing_message_builder.rb' - -# Offense count: 4 -# Cop supports --auto-correct. -# Configuration parameters: MaxKeyValuePairs. -Performance/RedundantMerge: - Exclude: - - 'app/controllers/api/v1/callbacks_controller.rb' - - 'app/models/message.rb' - -# Offense count: 1 -# Cop supports --auto-correct. -Performance/StringReplacement: - Exclude: - - 'lib/events/base.rb' - -# Offense count: 4 -# Configuration parameters: Prefixes. -# Prefixes: when, with, without -RSpec/ContextWording: - Exclude: - - 'spec/models/contact_spec.rb' - - 'spec/models/user_spec.rb' - -# Offense count: 1 -RSpec/DescribeClass: - Exclude: - - 'spec/mailers/confirmation_instructions_spec.rb' - -# Offense count: 1 -RSpec/DescribeSymbol: - Exclude: - - 'spec/mailers/confirmation_instructions_spec.rb' - -# Offense count: 1 -# Cop supports --auto-correct. -# Configuration parameters: AllowConsecutiveOneLiners. -RSpec/EmptyLineAfterExample: - Exclude: - - 'spec/models/user_spec.rb' - -# Offense count: 1 -# Configuration parameters: Max. -RSpec/ExampleLength: - Exclude: - - 'spec/models/conversation_spec.rb' - -# Offense count: 1 -# Cop supports --auto-correct. -# Configuration parameters: EnforcedStyle. -# SupportedStyles: single_line_only, single_statement_only, disallow -RSpec/ImplicitSubject: - Exclude: - - 'spec/models/user_spec.rb' - -# Offense count: 7 -# Configuration parameters: AggregateFailuresByDefault. -RSpec/MultipleExpectations: - Max: 7 - -# Offense count: 1 -# Cop supports --auto-correct. -# Configuration parameters: EnforcedStyle. -# SupportedStyles: not_to, to_not -RSpec/NotToNot: - Exclude: - - 'spec/mailers/confirmation_instructions_spec.rb' - -# Offense count: 1 -# Configuration parameters: IgnoreNameless, IgnoreSymbolicNames. -RSpec/VerifiedDoubles: - Exclude: - - 'spec/models/conversation_spec.rb' - -# Offense count: 4 -# Cop supports --auto-correct. -Rails/ActiveRecordAliases: - Exclude: - - 'app/controllers/api/v1/agents_controller.rb' - - 'app/controllers/api/v1/callbacks_controller.rb' - - 'app/controllers/api/v1/canned_responses_controller.rb' - - 'app/controllers/api/v1/contacts_controller.rb' - -# Offense count: 2 -# Cop supports --auto-correct. -Rails/BelongsTo: - Exclude: - - 'app/models/message.rb' - - 'app/models/user.rb' - -# Offense count: 6 -# Cop supports --auto-correct. -# Configuration parameters: Include. -# Include: app/models/**/*.rb -Rails/EnumHash: - Exclude: - - 'app/models/attachment.rb' - - 'app/models/conversation.rb' - - 'app/models/message.rb' - - 'app/models/user.rb' - -# Offense count: 1 -# Configuration parameters: Include. -# Include: app/models/**/*.rb -Rails/HasManyOrHasOneDependent: - Exclude: - - 'app/models/user.rb' - -# Offense count: 1 -# Configuration parameters: Include. -# Include: app/models/**/*.rb -Rails/InverseOf: - Exclude: - - 'app/models/user.rb' - -# Offense count: 1 -# Configuration parameters: Include. -# Include: app/controllers/**/*.rb -Rails/LexicallyScopedActionFilter: - Exclude: - - 'app/controllers/home_controller.rb' - -# Offense count: 2 -# Configuration parameters: Include. -# Include: app/**/*.rb, config/**/*.rb, db/**/*.rb, lib/**/*.rb -Rails/Output: - Exclude: - - 'app/bot/bot.rb' - - 'app/builders/account_builder.rb' - -# Offense count: 7 -# Cop supports --auto-correct. -# Configuration parameters: EnforcedStyle. -# SupportedStyles: strict, flexible -Rails/TimeZone: - Exclude: - - 'app/builders/report_builder.rb' - - 'lib/reports/update_account_identity.rb' - - 'lib/reports/update_agent_identity.rb' - - 'lib/reports/update_identity.rb' - - 'spec/models/conversation_spec.rb' - -# Offense count: 8 -# Cop supports --auto-correct. -# Configuration parameters: Include. -# Include: app/models/**/*.rb -Rails/Validation: - Exclude: - - 'app/models/canned_response.rb' - - 'app/models/channel/facebook_page.rb' - - 'app/models/facebook_page.rb' - - 'app/models/telegram_bot.rb' - - 'app/models/user.rb' - -# Offense count: 15 -# Cop supports --auto-correct. -# Configuration parameters: AutoCorrect, EnforcedStyle. -# SupportedStyles: nested, compact -Style/ClassAndModuleChildren: - Exclude: - - 'app/builders/messages/message_builder.rb' - - 'app/controllers/api/v1/inbox_members_controller.rb' - - 'app/models/channel/facebook_page.rb' - - 'app/models/channel/web_widget.rb' - - 'app/presenters/conversations/event_data_presenter.rb' - - 'app/services/facebook/send_reply_service.rb' - - 'lib/integrations/facebook/delivery_status.rb' - - 'lib/integrations/facebook/message_creator.rb' - - 'lib/integrations/facebook/message_parser.rb' - - 'lib/integrations/widget/incoming_message_builder.rb' - -# Offense count: 4 -Style/CommentedKeyword: - Exclude: - - 'app/controllers/api/v1/callbacks_controller.rb' - - 'app/controllers/api/v1/conversations/assignments_controller.rb' - - 'app/controllers/api/v1/conversations/labels_controller.rb' - - 'app/controllers/api/v1/labels_controller.rb' - -# Offense count: 1 -# Configuration parameters: AllowIfModifier. -Style/IfInsideElse: - Exclude: - - 'app/finders/conversation_finder.rb' - -# Offense count: 1 -Style/MixinUsage: - Exclude: - - 'app/bot/bot.rb' - -# Offense count: 1 -# Cop supports --auto-correct. -# Configuration parameters: AutoCorrect, EnforcedStyle, IgnoredMethods. -# SupportedStyles: predicate, comparison -Style/NumericPredicate: - Exclude: - - 'spec/**/*' - - 'app/controllers/api/v1/callbacks_controller.rb' - -# Offense count: 1 -# Cop supports --auto-correct. -# Configuration parameters: EnforcedStyle. -# SupportedStyles: implicit, explicit -Style/RescueStandardError: - Exclude: - - 'app/models/channel/facebook_page.rb' diff --git a/Gemfile b/Gemfile index 0f9a1d5ba..a21af8c01 100644 --- a/Gemfile +++ b/Gemfile @@ -39,6 +39,8 @@ gem 'rack-attack', '>= 6.7.0' gem 'down' # authentication type to fetch and send mail over oauth2.0 gem 'gmail_xoauth' +# Lock net-smtp to 0.3.4 to avoid issues with gmail_xoauth2 +gem 'net-smtp', '~> 0.3.4' # Prevent CSV injection gem 'csv-safe' @@ -74,7 +76,7 @@ gem 'devise_token_auth' gem 'jwt' gem 'pundit' # super admin -gem 'administrate', '>= 0.19.0' +gem 'administrate', '>= 0.20.1' gem 'administrate-field-active_storage', '>= 1.0.1' gem 'administrate-field-belongs_to_search', '>= 0.9.0' diff --git a/Gemfile.lock b/Gemfile.lock index 08c2696c6..74b281b43 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -105,12 +105,12 @@ GEM activerecord (>= 6.0, < 7.1) addressable (2.8.4) public_suffix (>= 2.0.2, < 6.0) - administrate (0.19.0) - actionpack (>= 5.0) - actionview (>= 5.0) - activerecord (>= 5.0) - jquery-rails (>= 4.0) - kaminari (>= 1.0) + administrate (0.20.1) + actionpack (>= 6.0, < 8.0) + actionview (>= 6.0, < 8.0) + activerecord (>= 6.0, < 8.0) + jquery-rails (~> 4.6.0) + kaminari (~> 1.2.2) sassc-rails (~> 2.1) selectize-rails (~> 0.6) administrate-field-active_storage (1.0.1) @@ -277,7 +277,7 @@ GEM gli (2.21.1) globalid (1.2.1) activesupport (>= 6.1) - gmail_xoauth (0.4.2) + gmail_xoauth (0.4.3) oauth (>= 0.3.6) google-apis-core (0.11.0) addressable (~> 2.5, >= 2.5.1) @@ -461,7 +461,7 @@ GEM mini_magick (4.12.0) mini_mime (1.1.5) mini_portile2 (2.8.5) - minitest (5.20.0) + minitest (5.21.2) mock_redis (0.36.0) ruby2_keywords msgpack (1.7.0) @@ -479,7 +479,7 @@ GEM net-protocol net-protocol (0.2.2) timeout - net-smtp (0.4.0) + net-smtp (0.3.4) net-protocol netrc (0.11.0) newrelic-sidekiq-metrics (1.6.2) @@ -488,14 +488,14 @@ GEM newrelic_rpm (9.6.0) base64 nio4r (2.7.0) - nokogiri (1.16.0) + nokogiri (1.16.2) mini_portile2 (~> 2.8.2) racc (~> 1.4) - nokogiri (1.16.0-arm64-darwin) + nokogiri (1.16.2-arm64-darwin) racc (~> 1.4) - nokogiri (1.16.0-x86_64-darwin) + nokogiri (1.16.2-x86_64-darwin) racc (~> 1.4) - nokogiri (1.16.0-x86_64-linux) + nokogiri (1.16.2-x86_64-linux) racc (~> 1.4) numo-narray (0.9.2.1) oauth (1.1.0) @@ -794,7 +794,7 @@ GEM valid_email2 (4.0.6) activemodel (>= 3.2) mail (~> 2.5) - version_gem (1.1.2) + version_gem (1.1.3) warden (1.2.9) rack (>= 2.0.9) web-console (4.2.1) @@ -840,7 +840,7 @@ DEPENDENCIES active_record_query_trace activerecord-import acts-as-taggable-on - administrate (>= 0.19.0) + administrate (>= 0.20.1) administrate-field-active_storage (>= 1.0.1) administrate-field-belongs_to_search (>= 0.9.0) annotate @@ -903,6 +903,7 @@ DEPENDENCIES meta_request mock_redis neighbor + net-smtp (~> 0.3.4) newrelic-sidekiq-metrics (>= 1.6.2) newrelic_rpm omniauth (>= 2.1.2) diff --git a/LICENSE b/LICENSE index f36fc7c53..8f80f35be 100644 --- a/LICENSE +++ b/LICENSE @@ -1,4 +1,4 @@ -Copyright (c) 2017-2021 Chatwoot Inc. +Copyright (c) 2017-2024 Chatwoot Inc. Portions of this software are licensed as follows: diff --git a/app/assets/stylesheets/administrate/custom_styles.scss b/app/assets/stylesheets/administrate/custom_styles.scss index 859a35689..5e6d803d8 100644 --- a/app/assets/stylesheets/administrate/custom_styles.scss +++ b/app/assets/stylesheets/administrate/custom_styles.scss @@ -9,7 +9,7 @@ padding: 4px 12px; .icon-container { - margin-right: 4px; + margin-right: 2px; } diff --git a/app/builders/account_builder.rb b/app/builders/account_builder.rb index 3e2ac9d6e..f179c6405 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!, :confirmed, :user, :user_full_name, :user_password, :super_admin, :locale] + pattr_initialize [:account_name, :email!, :confirmed, :user, :user_full_name, :user_password, :super_admin, :locale] def perform if @user.nil? @@ -15,12 +15,22 @@ class AccountBuilder end [@user, @account] rescue StandardError => e - puts e.inspect + Rails.logger.debug e.inspect raise e end private + def user_full_name + # the empty string ensures that not-null constraint is not violated + @user_full_name || '' + end + + def account_name + # the empty string ensures that not-null constraint is not violated + @account_name || '' + end + def validate_email address = ValidEmail2::Address.new(@email) if address.valid? # && !address.disposable? @@ -39,7 +49,7 @@ class AccountBuilder end def create_account - @account = Account.create!(name: @account_name, locale: I18n.locale) + @account = Account.create!(name: account_name, locale: I18n.locale) Current.account = @account end @@ -64,7 +74,7 @@ class AccountBuilder @user = User.new(email: @email, password: user_password, password_confirmation: user_password, - name: @user_full_name) + name: user_full_name) @user.type = 'SuperAdmin' if @super_admin @user.confirm if @confirmed @user.save! diff --git a/app/builders/agent_builder.rb b/app/builders/agent_builder.rb new file mode 100644 index 000000000..6ea68821d --- /dev/null +++ b/app/builders/agent_builder.rb @@ -0,0 +1,60 @@ +# The AgentBuilder class is responsible for creating a new agent. +# It initializes with necessary attributes and provides a perform method +# to create a user and account user in a transaction. +class AgentBuilder + # Initializes an AgentBuilder with necessary attributes. + # @param email [String] the email of the user. + # @param name [String] the name of the user. + # @param role [String] the role of the user, defaults to 'agent' if not provided. + # @param inviter [User] the user who is inviting the agent (Current.user in most cases). + # @param availability [String] the availability status of the user, defaults to 'offline' if not provided. + # @param auto_offline [Boolean] the auto offline status of the user. + pattr_initialize [:email, { name: '' }, :inviter, :account, { role: :agent }, { availability: :offline }, { auto_offline: false }] + + # Creates a user and account user in a transaction. + # @return [User] the created user. + def perform + ActiveRecord::Base.transaction do + @user = find_or_create_user + send_confirmation_if_required + create_account_user + end + @user + end + + private + + # Finds a user by email or creates a new one with a temporary password. + # @return [User] the found or created user. + def find_or_create_user + user = User.find_by(email: email) + return user if user + + temp_password = "1!aA#{SecureRandom.alphanumeric(12)}" + User.create!(email: email, name: name, password: temp_password, password_confirmation: temp_password) + end + + # Sends confirmation instructions if the user is persisted and not confirmed. + def send_confirmation_if_required + @user.send_confirmation_instructions if user_needs_confirmation? + end + + # Checks if the user needs confirmation. + # @return [Boolean] true if the user is persisted and not confirmed, false otherwise. + def user_needs_confirmation? + @user.persisted? && !@user.confirmed? + end + + # Creates an account user linking the user to the current account. + def create_account_user + AccountUser.create!({ + account_id: account.id, + user_id: @user.id, + inviter_id: inviter.id + }.merge({ + role: role, + availability: availability, + auto_offline: auto_offline + }.compact)) + end +end diff --git a/app/controllers/api/v1/accounts/agents_controller.rb b/app/controllers/api/v1/accounts/agents_controller.rb index 9a089f242..768d1a3ff 100644 --- a/app/controllers/api/v1/accounts/agents_controller.rb +++ b/app/controllers/api/v1/accounts/agents_controller.rb @@ -1,16 +1,26 @@ class Api::V1::Accounts::AgentsController < Api::V1::Accounts::BaseController - before_action :fetch_agent, except: [:create, :index] + before_action :fetch_agent, except: [:create, :index, :bulk_create] before_action :check_authorization - before_action :find_user, only: [:create] before_action :validate_limit, only: [:create] - before_action :create_user, only: [:create] - before_action :save_account_user, only: [:create] + before_action :validate_limit_for_bulk_create, only: [:bulk_create] def index @agents = agents end - def create; end + def create + builder = AgentBuilder.new( + email: new_agent_params['email'], + name: new_agent_params['name'], + role: new_agent_params['role'], + availability: new_agent_params['availability'], + auto_offline: new_agent_params['auto_offline'], + inviter: current_user, + account: Current.account + ) + + builder.perform + end def update @agent.update!(agent_params.slice(:name).compact) @@ -23,6 +33,21 @@ class Api::V1::Accounts::AgentsController < Api::V1::Accounts::BaseController head :ok end + def bulk_create + emails = params[:emails] + + emails.each do |email| + builder = AgentBuilder.new( + email: email, + name: email.split('@').first, + inviter: current_user, + account: Current.account + ) + builder.perform + end + head :ok + end + private def check_authorization @@ -33,47 +58,34 @@ class Api::V1::Accounts::AgentsController < Api::V1::Accounts::BaseController @agent = agents.find(params[:id]) end - def find_user - @user = User.find_by(email: new_agent_params[:email]) - end - - # TODO: move this to a builder and combine the save account user method into a builder - # ensure the account user association is also created in a single transaction - def create_user - return @user.send_confirmation_instructions if @user - - @user = User.create!(new_agent_params.slice(:email, :name, :password, :password_confirmation)) - end - - def save_account_user - AccountUser.create!({ - account_id: Current.account.id, - user_id: @user.id, - inviter_id: current_user.id - }.merge({ - role: new_agent_params[:role], - availability: new_agent_params[:availability], - auto_offline: new_agent_params[:auto_offline] - }.compact)) - end - def agent_params params.require(:agent).permit(:name, :email, :name, :role, :availability, :auto_offline) end def new_agent_params - # intial string ensures the password requirements are met - temp_password = "1!aA#{SecureRandom.alphanumeric(12)}" params.require(:agent).permit(:email, :name, :role, :availability, :auto_offline) - .merge!(password: temp_password, password_confirmation: temp_password, inviter: current_user) end def agents @agents ||= Current.account.users.order_by_full_name.includes(:account_users, { avatar_attachment: [:blob] }) end + def validate_limit_for_bulk_create + limit_available = params[:emails].count <= available_agent_count + + render_payment_required('Account limit exceeded. Please purchase more licenses') unless limit_available + end + def validate_limit - render_payment_required('Account limit exceeded. Please purchase more licenses') if agents.count >= Current.account.usage_limits[:agents] + render_payment_required('Account limit exceeded. Please purchase more licenses') unless can_add_agent? + end + + def available_agent_count + Current.account.usage_limits[:agents] - agents.count + end + + def can_add_agent? + available_agent_count.positive? end def delete_user_record(agent) diff --git a/app/controllers/api/v1/accounts/notifications_controller.rb b/app/controllers/api/v1/accounts/notifications_controller.rb index a3d8df27e..0eeff5695 100644 --- a/app/controllers/api/v1/accounts/notifications_controller.rb +++ b/app/controllers/api/v1/accounts/notifications_controller.rb @@ -2,7 +2,7 @@ class Api::V1::Accounts::NotificationsController < Api::V1::Accounts::BaseContro RESULTS_PER_PAGE = 15 include DateRangeHelper - before_action :fetch_notification, only: [:update, :destroy, :snooze] + before_action :fetch_notification, only: [:update, :destroy, :snooze, :unread] before_action :set_primary_actor, only: [:read_all] before_action :set_current_page, only: [:index] @@ -29,11 +29,25 @@ class Api::V1::Accounts::NotificationsController < Api::V1::Accounts::BaseContro render json: @notification end + def unread + @notification.update(read_at: nil) + render json: @notification + end + def destroy @notification.destroy head :ok end + def destroy_all + if params[:type] == 'read' + ::Notification::DeleteNotificationJob.perform_later(Current.user, type: :read) + else + ::Notification::DeleteNotificationJob.perform_later(Current.user, type: :all) + end + head :ok + end + def unread_count @unread_count = notification_finder.unread_count render json: @unread_count diff --git a/app/controllers/api/v1/accounts_controller.rb b/app/controllers/api/v1/accounts_controller.rb index ef0e0c777..0e5615bae 100644 --- a/app/controllers/api/v1/accounts_controller.rb +++ b/app/controllers/api/v1/accounts_controller.rb @@ -5,11 +5,13 @@ class Api::V1::AccountsController < Api::BaseController skip_before_action :authenticate_user!, :set_current_user, :handle_with_exception, only: [:create], raise: false before_action :check_signup_enabled, only: [:create] + before_action :ensure_account_name, only: [:create] before_action :validate_captcha, only: [:create] before_action :fetch_account, except: [:create] before_action :check_authorization, except: [:create] rescue_from CustomExceptions::Account::InvalidEmail, + CustomExceptions::Account::InvalidParams, CustomExceptions::Account::UserExists, CustomExceptions::Account::UserErrors, with: :render_error_response @@ -38,7 +40,7 @@ class Api::V1::AccountsController < Api::BaseController def cache_keys expires_in 10.seconds, public: false, stale_while_revalidate: 5.minutes - render json: { cache_keys: get_cache_keys }, status: :ok + render json: { cache_keys: cache_keys_for_account }, status: :ok end def update @@ -53,7 +55,18 @@ class Api::V1::AccountsController < Api::BaseController private - def get_cache_keys + def ensure_account_name + # ensure that account_name and user_full_name is present + # this is becuase the account builder and the models validations are not triggered + # this change is to align the behaviour with the v2 accounts controller + # since these values are not required directly there + return if account_params[:account_name].present? + return if account_params[:user_full_name].present? + + raise CustomExceptions::Account::InvalidParams.new({}) + end + + def cache_keys_for_account { label: fetch_value_for_key(params[:id], Label.name.underscore), inbox: fetch_value_for_key(params[:id], Inbox.name.underscore), diff --git a/app/controllers/api/v1/profiles_controller.rb b/app/controllers/api/v1/profiles_controller.rb index cbf801e82..a77652ff8 100644 --- a/app/controllers/api/v1/profiles_controller.rb +++ b/app/controllers/api/v1/profiles_controller.rb @@ -31,6 +31,11 @@ class Api::V1::ProfilesController < Api::BaseController head :ok end + def resend_confirmation + @user.send_confirmation_instructions unless @user.confirmed? + head :ok + end + private def set_user diff --git a/app/controllers/api/v2/accounts_controller.rb b/app/controllers/api/v2/accounts_controller.rb new file mode 100644 index 000000000..e38863c07 --- /dev/null +++ b/app/controllers/api/v2/accounts_controller.rb @@ -0,0 +1,48 @@ +class Api::V2::AccountsController < Api::BaseController + include AuthHelper + + skip_before_action :authenticate_user!, :set_current_user, :handle_with_exception, + only: [:create], raise: false + before_action :check_signup_enabled, only: [:create] + before_action :validate_captcha, only: [:create] + before_action :fetch_account, except: [:create] + before_action :check_authorization, except: [:create] + + rescue_from CustomExceptions::Account::InvalidEmail, + CustomExceptions::Account::UserExists, + CustomExceptions::Account::UserErrors, + with: :render_error_response + + def create + @user, @account = AccountBuilder.new( + email: account_params[:email], + user_password: account_params[:password], + user: current_user + ).perform + if @user + send_auth_headers(@user) + render 'api/v1/accounts/create', format: :json, locals: { resource: @user } + else + render_error_response(CustomExceptions::Account::SignupFailed.new({})) + end + end + + private + + 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 + params.permit(:account_name, :email, :name, :password, :locale, :domain, :support_email, :auto_resolve_duration, :user_full_name) + end + + def check_signup_enabled + raise ActionController::RoutingError, 'Not Found' if GlobalConfigService.load('ENABLE_ACCOUNT_SIGNUP', 'false') == 'false' + end + + def validate_captcha + raise ActionController::InvalidAuthenticityToken, 'Invalid Captcha' unless ChatwootCaptcha.new(params[:h_captcha_client_response]).valid? + end +end diff --git a/app/controllers/super_admin/app_configs_controller.rb b/app/controllers/super_admin/app_configs_controller.rb index 97caad42c..a31d01675 100644 --- a/app/controllers/super_admin/app_configs_controller.rb +++ b/app/controllers/super_admin/app_configs_controller.rb @@ -22,19 +22,24 @@ class SuperAdmin::AppConfigsController < SuperAdmin::ApplicationController i.value = value i.save! end - # rubocop:disable Rails/I18nLocaleTexts - redirect_to super_admin_settings_path, notice: 'App Configs updated successfully' - # rubocop:enable Rails/I18nLocaleTexts + redirect_to super_admin_settings_path, notice: "App Configs - #{@config.titleize} updated successfully" end private def set_config - @config = params[:config] + @config = params[:config] || 'general' end def allowed_configs - @allowed_configs = %w[FB_APP_ID FB_VERIFY_TOKEN FB_APP_SECRET] + @allowed_configs = case @config + when 'facebook' + %w[FB_APP_ID FB_VERIFY_TOKEN FB_APP_SECRET IG_VERIFY_TOKEN ENABLE_MESSENGER_CHANNEL_HUMAN_AGENT] + when 'email' + ['MAILER_INBOUND_EMAIL_DOMAIN'] + else + %w[ENABLE_ACCOUNT_SIGNUP] + end end end diff --git a/app/finders/notification_finder.rb b/app/finders/notification_finder.rb index 7559d6ef9..7e5789372 100644 --- a/app/finders/notification_finder.rb +++ b/app/finders/notification_finder.rb @@ -26,6 +26,7 @@ class NotificationFinder def set_up find_all_notifications + filter_by_read_status filter_by_status end @@ -37,11 +38,15 @@ class NotificationFinder @notifications = @notifications.where('snoozed_until > ?', DateTime.now.utc) if params[:status] == 'snoozed' end + def filter_by_read_status + @notifications = @notifications.where.not(read_at: nil) if params[:type] == 'read' + end + def current_page params[:page] || 1 end def notifications - @notifications.page(current_page).per(RESULTS_PER_PAGE).order(last_activity_at: :desc) + @notifications.page(current_page).per(RESULTS_PER_PAGE).order(last_activity_at: params[:sort_order] || :desc) end end diff --git a/app/helpers/super_admin/account_features_helper.rb b/app/helpers/super_admin/account_features_helper.rb new file mode 100644 index 000000000..759aec134 --- /dev/null +++ b/app/helpers/super_admin/account_features_helper.rb @@ -0,0 +1,9 @@ +module SuperAdmin::AccountFeaturesHelper + def self.account_features + YAML.safe_load(Rails.root.join('config/features.yml').read).freeze + end + + def self.account_premium_features + account_features.filter { |feature| feature['premium'] }.pluck('name') + end +end diff --git a/app/javascript/dashboard/App.vue b/app/javascript/dashboard/App.vue index 6fd470fcf..49fe325f6 100644 --- a/app/javascript/dashboard/App.vue +++ b/app/javascript/dashboard/App.vue @@ -2,12 +2,13 @@
@@ -32,6 +33,7 @@ import NetworkNotification from './components/NetworkNotification.vue'; import UpdateBanner from './components/app/UpdateBanner.vue'; import UpgradeBanner from './components/app/UpgradeBanner.vue'; import PaymentPendingBanner from './components/app/PaymentPendingBanner.vue'; +import PendingEmailVerificationBanner from './components/app/PendingEmailVerificationBanner.vue'; import vueActionCable from './helper/actionCable'; import WootSnackbarBox from './components/SnackbarContainer.vue'; import rtlMixin from 'shared/mixins/rtlMixin'; @@ -52,6 +54,7 @@ export default { PaymentPendingBanner, WootSnackbarBox, UpgradeBanner, + PendingEmailVerificationBanner, }, mixins: [rtlMixin], diff --git a/app/javascript/dashboard/api/auth.js b/app/javascript/dashboard/api/auth.js index 48e7d7085..dde817866 100644 --- a/app/javascript/dashboard/api/auth.js +++ b/app/javascript/dashboard/api/auth.js @@ -29,11 +29,12 @@ export default { return fetchPromise; }, hasAuthCookie() { - return !!Cookies.getJSON('cw_d_session_info'); + return !!Cookies.get('cw_d_session_info'); }, getAuthData() { if (this.hasAuthCookie()) { - return Cookies.getJSON('cw_d_session_info'); + const savedAuthInfo = Cookies.get('cw_d_session_info'); + return JSON.parse(savedAuthInfo || '{}'); } return false; }, @@ -97,4 +98,8 @@ export default { }, }); }, + resendConfirmation() { + const urlData = endPoints('resendConfirmation'); + return axios.post(urlData.url); + }, }; diff --git a/app/javascript/dashboard/api/endPoints.js b/app/javascript/dashboard/api/endPoints.js index 678386d50..31337b7fc 100644 --- a/app/javascript/dashboard/api/endPoints.js +++ b/app/javascript/dashboard/api/endPoints.js @@ -47,6 +47,10 @@ const endPoints = { setActiveAccount: { url: '/api/v1/profile/set_active_account', }, + + resendConfirmation: { + url: '/api/v1/profile/resend_confirmation', + }, }; export default page => { diff --git a/app/javascript/dashboard/api/notifications.js b/app/javascript/dashboard/api/notifications.js index e13bc78a6..65fc937b7 100644 --- a/app/javascript/dashboard/api/notifications.js +++ b/app/javascript/dashboard/api/notifications.js @@ -6,8 +6,15 @@ class NotificationsAPI extends ApiClient { super('notifications', { accountScoped: true }); } - get(page) { - return axios.get(`${this.url}?page=${page}`); + get({ page, status, type, sortOrder }) { + return axios.get(this.url, { + params: { + page, + status, + type, + sort_order: sortOrder, + }, + }); } getNotifications(contactId) { @@ -25,9 +32,29 @@ class NotificationsAPI extends ApiClient { }); } + unRead(id) { + return axios.post(`${this.url}/${id}/unread`); + } + readAll() { return axios.post(`${this.url}/read_all`); } + + delete(id) { + return axios.delete(`${this.url}/${id}`); + } + + deleteAll({ type = 'all' }) { + return axios.post(`${this.url}/destroy_all`, { + type, + }); + } + + snooze({ id, snoozedUntil = null }) { + return axios.post(`${this.url}/${id}/snooze`, { + snoozed_until: snoozedUntil, + }); + } } export default new NotificationsAPI(); diff --git a/app/javascript/dashboard/api/specs/notifications.spec.js b/app/javascript/dashboard/api/specs/notifications.spec.js index 5bdf88d7f..bc06eaa2b 100644 --- a/app/javascript/dashboard/api/specs/notifications.spec.js +++ b/app/javascript/dashboard/api/specs/notifications.spec.js @@ -28,10 +28,20 @@ describe('#NotificationAPI', () => { }); it('#get', () => { - notificationsAPI.get(1); - expect(axiosMock.get).toHaveBeenCalledWith( - '/api/v1/notifications?page=1' - ); + notificationsAPI.get({ + page: 1, + status: 'read', + type: 'Conversation', + sortOrder: 'desc', + }); + expect(axiosMock.get).toHaveBeenCalledWith('/api/v1/notifications', { + params: { + page: 1, + status: 'read', + type: 'Conversation', + sort_order: 'desc', + }, + }); }); it('#getNotifications', () => { @@ -65,5 +75,30 @@ describe('#NotificationAPI', () => { '/api/v1/notifications/read_all' ); }); + + it('#snooze', () => { + notificationsAPI.snooze({ id: 1, snoozedUntil: 12332211 }); + expect(axiosMock.post).toHaveBeenCalledWith( + '/api/v1/notifications/1/snooze', + { + snoozed_until: 12332211, + } + ); + }); + + it('#delete', () => { + notificationsAPI.delete(1); + expect(axiosMock.delete).toHaveBeenCalledWith('/api/v1/notifications/1'); + }); + + it('#deleteAll', () => { + notificationsAPI.deleteAll({ type: 'all' }); + expect(axiosMock.post).toHaveBeenCalledWith( + '/api/v1/notifications/destroy_all', + { + type: 'all', + } + ); + }); }); }); diff --git a/app/javascript/dashboard/components/ChatList.vue b/app/javascript/dashboard/components/ChatList.vue index 31fe6110d..7fcc58127 100644 --- a/app/javascript/dashboard/components/ChatList.vue +++ b/app/javascript/dashboard/components/ChatList.vue @@ -1,6 +1,6 @@ + + diff --git a/app/javascript/dashboard/components/ui/Banner.vue b/app/javascript/dashboard/components/ui/Banner.vue index 5d680fe48..3473111ae 100644 --- a/app/javascript/dashboard/components/ui/Banner.vue +++ b/app/javascript/dashboard/components/ui/Banner.vue @@ -1,6 +1,6 @@