mirror of
				https://github.com/lingble/chatwoot.git
				synced 2025-10-31 19:17:48 +00:00 
			
		
		
		
	feat: Improved password security policy (#2345)
Co-authored-by: Pranav Raj S <pranav@chatwoot.com>
This commit is contained in:
		
							
								
								
									
										1
									
								
								Gemfile
									
									
									
									
									
								
							
							
						
						
									
										1
									
								
								Gemfile
									
									
									
									
									
								
							| @@ -59,6 +59,7 @@ gem 'barnes' | ||||
|  | ||||
| ##--- gems for authentication & authorization ---## | ||||
| gem 'devise' | ||||
| gem 'devise-secure_password', '~> 2.0' | ||||
| gem 'devise_token_auth' | ||||
| # authorization | ||||
| gem 'jwt' | ||||
|   | ||||
							
								
								
									
										16
									
								
								Gemfile.lock
									
									
									
									
									
								
							
							
						
						
									
										16
									
								
								Gemfile.lock
									
									
									
									
									
								
							| @@ -123,7 +123,7 @@ GEM | ||||
|     barnes (0.0.8) | ||||
|       multi_json (~> 1) | ||||
|       statsd-ruby (~> 1.1) | ||||
|     bcrypt (3.1.15) | ||||
|     bcrypt (3.1.16) | ||||
|     bindex (0.8.1) | ||||
|     bootsnap (1.4.8) | ||||
|       msgpack (~> 1.0) | ||||
| @@ -158,12 +158,15 @@ GEM | ||||
|     declarative-option (0.1.0) | ||||
|     descendants_tracker (0.0.4) | ||||
|       thread_safe (~> 0.3, >= 0.3.1) | ||||
|     devise (4.7.2) | ||||
|     devise (4.8.0) | ||||
|       bcrypt (~> 3.0) | ||||
|       orm_adapter (~> 0.1) | ||||
|       railties (>= 4.1.0) | ||||
|       responders | ||||
|       warden (~> 1.2.3) | ||||
|     devise-secure_password (2.0.1) | ||||
|       devise (>= 4.0.0, < 5.0.0) | ||||
|       railties (>= 5.0.0, < 7.0.0) | ||||
|     devise_token_auth (1.1.4) | ||||
|       bcrypt (~> 3.0) | ||||
|       devise (> 3.5.2, < 5) | ||||
| @@ -259,7 +262,7 @@ GEM | ||||
|       multi_json (~> 1.11) | ||||
|       os (>= 0.9, < 2.0) | ||||
|       signet (~> 0.14) | ||||
|     groupdate (5.1.0) | ||||
|     groupdate (5.2.2) | ||||
|       activesupport (>= 5) | ||||
|     grpc (1.37.1) | ||||
|       google-protobuf (~> 3.15) | ||||
| @@ -348,7 +351,7 @@ GEM | ||||
|       connection_pool (~> 2.2) | ||||
|     netrc (0.11.0) | ||||
|     nio4r (2.5.7) | ||||
|     nokogiri (1.11.4) | ||||
|     nokogiri (1.11.6) | ||||
|       mini_portile2 (~> 2.5.0) | ||||
|       racc (~> 1.4) | ||||
|     oauth (0.5.6) | ||||
| @@ -582,8 +585,8 @@ GEM | ||||
|       coercible (~> 1.0) | ||||
|       descendants_tracker (~> 0.0, >= 0.0.3) | ||||
|       equalizer (~> 0.0, >= 0.0.9) | ||||
|     warden (1.2.8) | ||||
|       rack (>= 2.0.6) | ||||
|     warden (1.2.9) | ||||
|       rack (>= 2.0.9) | ||||
|     web-console (4.0.4) | ||||
|       actionview (>= 6.0.0) | ||||
|       activemodel (>= 6.0.0) | ||||
| @@ -629,6 +632,7 @@ DEPENDENCIES | ||||
|   cypress-on-rails (~> 1.0) | ||||
|   database_cleaner | ||||
|   devise | ||||
|   devise-secure_password (~> 2.0) | ||||
|   devise_token_auth | ||||
|   dotenv-rails | ||||
|   facebook-messenger | ||||
|   | ||||
| @@ -2,7 +2,7 @@ | ||||
|  | ||||
| class AccountBuilder | ||||
|   include CustomExceptions::Account | ||||
|   pattr_initialize [:account_name!, :email!, :confirmed!, :user, :user_full_name, :user_password] | ||||
|   pattr_initialize [:account_name!, :email!, :confirmed, :user, :user_full_name, :user_password] | ||||
|  | ||||
|   def perform | ||||
|     if @user.nil? | ||||
| @@ -61,11 +61,9 @@ class AccountBuilder | ||||
|   end | ||||
|  | ||||
|   def create_user | ||||
|     password = user_password || SecureRandom.alphanumeric(12) | ||||
|  | ||||
|     @user = User.new(email: @email, | ||||
|                      password: password, | ||||
|                      password_confirmation: password, | ||||
|                      password: user_password, | ||||
|                      password_confirmation: user_password, | ||||
|                      name: @user_full_name) | ||||
|     @user.confirm if @confirmed | ||||
|     @user.save! | ||||
|   | ||||
| @@ -58,9 +58,10 @@ class Api::V1::Accounts::AgentsController < Api::V1::Accounts::BaseController | ||||
|   end | ||||
|  | ||||
|   def new_agent_params | ||||
|     time = Time.now.to_i | ||||
|     # intial string ensures the password requirements are met | ||||
|     temp_password = "1!aA#{SecureRandom.alphanumeric(12)}" | ||||
|     params.require(:agent).permit(:email, :name, :role) | ||||
|           .merge!(password: time, password_confirmation: time, inviter: current_user) | ||||
|           .merge!(password: temp_password, password_confirmation: temp_password, inviter: current_user) | ||||
|   end | ||||
|  | ||||
|   def agents | ||||
|   | ||||
| @@ -18,7 +18,7 @@ class Api::V1::AccountsController < Api::BaseController | ||||
|       account_name: account_params[:account_name], | ||||
|       user_full_name: account_params[:user_full_name], | ||||
|       email: account_params[:email], | ||||
|       confirmed: confirmed?, | ||||
|       user_password: account_params[:password], | ||||
|       user: current_user | ||||
|     ).perform | ||||
|     if @user | ||||
| @@ -46,17 +46,13 @@ class Api::V1::AccountsController < Api::BaseController | ||||
|  | ||||
|   private | ||||
|  | ||||
|   def confirmed? | ||||
|     super_admin? && params[:confirmed] | ||||
|   end | ||||
|  | ||||
|   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, :locale, :domain, :support_email, :auto_resolve_duration, :user_full_name) | ||||
|     params.permit(:account_name, :email, :name, :password, :locale, :domain, :support_email, :auto_resolve_duration, :user_full_name) | ||||
|   end | ||||
|  | ||||
|   def check_signup_enabled | ||||
|   | ||||
| @@ -20,7 +20,7 @@ class Api::V1::Widget::MessagesController < Api::V1::Widget::BaseController | ||||
|       @message.update!(message_update_params[:message]) | ||||
|     end | ||||
|   rescue StandardError => e | ||||
|     render json: { error: @contact.errors, message: e.message }.to_json, status: 500 | ||||
|     render json: { error: @contact.errors, message: e.message }.to_json, status: :internal_server_error | ||||
|   end | ||||
|  | ||||
|   private | ||||
|   | ||||
| @@ -17,13 +17,8 @@ module AccessTokenAuthHelper | ||||
|     Current.user = @resource if current_user.is_a?(User) | ||||
|   end | ||||
|  | ||||
|   def super_admin? | ||||
|     @resource.present? && @resource.is_a?(SuperAdmin) | ||||
|   end | ||||
|  | ||||
|   def validate_bot_access_token! | ||||
|     return if Current.user.is_a?(User) | ||||
|     return if super_admin? | ||||
|     return if agent_bot_accessible? | ||||
|  | ||||
|     render_unauthorized('Access to this endpoint is not authorized for bots') | ||||
|   | ||||
| @@ -1,34 +1,29 @@ | ||||
| class DeviseOverrides::ConfirmationsController < Devise::ConfirmationsController | ||||
|   include AuthHelper | ||||
|   skip_before_action :require_no_authentication, raise: false | ||||
|   skip_before_action :authenticate_user!, raise: false | ||||
|  | ||||
|   def create | ||||
|     @confirmable = User.find_by(confirmation_token: params[:confirmation_token]) | ||||
|     render_confirmation_success and return if @confirmable&.confirm | ||||
|  | ||||
|     if confirm | ||||
|       render_confirmation_success | ||||
|     else | ||||
|     render_confirmation_error | ||||
|   end | ||||
|   end | ||||
|  | ||||
|   protected | ||||
|  | ||||
|   def confirm | ||||
|     @confirmable&.confirm || (@confirmable&.confirmed_at && @confirmable&.reset_password_token) | ||||
|   end | ||||
|   private | ||||
|  | ||||
|   def render_confirmation_success | ||||
|     render json: { "message": 'Success', "redirect_url": create_reset_token_link(@confirmable) }, status: :ok | ||||
|     send_auth_headers(@confirmable) | ||||
|     render partial: 'devise/auth.json', locals: { resource: @confirmable } | ||||
|   end | ||||
|  | ||||
|   def render_confirmation_error | ||||
|     if @confirmable.blank? | ||||
|       render json: { "message": 'Invalid token', "redirect_url": '/' }, status: 422 | ||||
|       render json: { message: 'Invalid token', redirect_url: '/' }, status: :unprocessable_entity | ||||
|     elsif @confirmable.confirmed_at | ||||
|       render json: { "message": 'Already confirmed', "redirect_url": '/' }, status: 422 | ||||
|       render json: { message: 'Already confirmed', redirect_url: '/' }, status: :unprocessable_entity | ||||
|     else | ||||
|       render json: { "message": 'Failure', "redirect_url": '/' }, status: 422 | ||||
|       render json: { message: 'Failure', redirect_url: '/' }, status: :unprocessable_entity | ||||
|     end | ||||
|   end | ||||
|  | ||||
|   | ||||
| @@ -13,7 +13,7 @@ class DeviseOverrides::PasswordsController < Devise::PasswordsController | ||||
|       send_auth_headers(@recoverable) | ||||
|       render partial: 'devise/auth.json', locals: { resource: @recoverable } | ||||
|     else | ||||
|       render json: { "message": 'Invalid token', "redirect_url": '/' }, status: 422 | ||||
|       render json: { message: 'Invalid token', redirect_url: '/' }, status: :unprocessable_entity | ||||
|     end | ||||
|   end | ||||
|  | ||||
| @@ -27,7 +27,7 @@ class DeviseOverrides::PasswordsController < Devise::PasswordsController | ||||
|     end | ||||
|   end | ||||
|  | ||||
|   protected | ||||
|   private | ||||
|  | ||||
|   def reset_password_and_confirmation(recoverable) | ||||
|     recoverable.confirm unless recoverable.confirmed? # confirm if user resets password without confirming anytime before | ||||
| @@ -40,7 +40,7 @@ class DeviseOverrides::PasswordsController < Devise::PasswordsController | ||||
|  | ||||
|   def build_response(message, status) | ||||
|     render json: { | ||||
|       "message": message | ||||
|       message: message | ||||
|     }, status: status | ||||
|   end | ||||
| end | ||||
|   | ||||
| @@ -11,7 +11,6 @@ class SuperAdminDashboard < Administrate::BaseDashboard | ||||
|     id: Field::Number, | ||||
|     email: Field::String, | ||||
|     password: Field::Password, | ||||
|     access_token: Field::HasOne, | ||||
|     remember_created_at: Field::DateTime, | ||||
|     sign_in_count: Field::Number, | ||||
|     current_sign_in_at: Field::DateTime, | ||||
| @@ -30,7 +29,6 @@ class SuperAdminDashboard < Administrate::BaseDashboard | ||||
|   COLLECTION_ATTRIBUTES = %i[ | ||||
|     id | ||||
|     email | ||||
|     access_token | ||||
|   ].freeze | ||||
|  | ||||
|   # SHOW_PAGE_ATTRIBUTES | ||||
|   | ||||
| @@ -29,6 +29,7 @@ export default { | ||||
|           account_name: creds.accountName.trim(), | ||||
|           user_full_name: creds.fullName.trim(), | ||||
|           email: creds.email, | ||||
|           password: creds.password, | ||||
|         }) | ||||
|         .then(response => { | ||||
|           setAuthCredentials(response); | ||||
| @@ -95,8 +96,18 @@ export default { | ||||
|   }, | ||||
|  | ||||
|   verifyPasswordToken({ confirmationToken }) { | ||||
|     return axios.post('auth/confirmation', { | ||||
|     return new Promise((resolve, reject) => { | ||||
|       axios | ||||
|         .post('auth/confirmation', { | ||||
|           confirmation_token: confirmationToken, | ||||
|         }) | ||||
|         .then(response => { | ||||
|           setAuthCredentials(response); | ||||
|           resolve(response); | ||||
|         }) | ||||
|         .catch(error => { | ||||
|           reject(error.response); | ||||
|         }); | ||||
|     }); | ||||
|   }, | ||||
|  | ||||
|   | ||||
| @@ -21,12 +21,10 @@ export default { | ||||
|   methods: { | ||||
|     async confirmToken() { | ||||
|       try { | ||||
|         const { | ||||
|           data: { redirect_url: redirectURL }, | ||||
|         } = await Auth.verifyPasswordToken({ | ||||
|         await Auth.verifyPasswordToken({ | ||||
|           confirmationToken: this.confirmationToken, | ||||
|         }); | ||||
|         window.location = redirectURL; | ||||
|         window.location = DEFAULT_REDIRECT_URL; | ||||
|       } catch (error) { | ||||
|         window.location = DEFAULT_REDIRECT_URL; | ||||
|       } | ||||
|   | ||||
| @@ -120,8 +120,12 @@ export default { | ||||
|             window.location = DEFAULT_REDIRECT_URL; | ||||
|           } | ||||
|         }) | ||||
|         .catch(() => { | ||||
|           this.showAlert(this.$t('SET_NEW_PASSWORD.API.ERROR_MESSAGE')); | ||||
|         .catch(error => { | ||||
|           let errorMessage = this.$t('SET_NEW_PASSWORD.API.ERROR_MESSAGE'); | ||||
|           if (error?.data?.message) { | ||||
|             errorMessage = error.data.message; | ||||
|           } | ||||
|           this.showAlert(errorMessage); | ||||
|         }); | ||||
|     }, | ||||
|   }, | ||||
|   | ||||
| @@ -25,6 +25,17 @@ | ||||
|             " | ||||
|             @blur="$v.credentials.fullName.$touch" | ||||
|           /> | ||||
|           <woot-input | ||||
|             v-model.trim="credentials.email" | ||||
|             type="email" | ||||
|             :class="{ error: $v.credentials.email.$error }" | ||||
|             :label="$t('REGISTER.EMAIL.LABEL')" | ||||
|             :placeholder="$t('REGISTER.EMAIL.PLACEHOLDER')" | ||||
|             :error=" | ||||
|               $v.credentials.email.$error ? $t('REGISTER.EMAIL.ERROR') : '' | ||||
|             " | ||||
|             @blur="$v.credentials.email.$touch" | ||||
|           /> | ||||
|           <woot-input | ||||
|             v-model="credentials.accountName" | ||||
|             :class="{ error: $v.credentials.accountName.$error }" | ||||
| @@ -38,15 +49,31 @@ | ||||
|             @blur="$v.credentials.accountName.$touch" | ||||
|           /> | ||||
|           <woot-input | ||||
|             v-model.trim="credentials.email" | ||||
|             type="email" | ||||
|             :class="{ error: $v.credentials.email.$error }" | ||||
|             :label="$t('REGISTER.EMAIL.LABEL')" | ||||
|             :placeholder="$t('REGISTER.EMAIL.PLACEHOLDER')" | ||||
|             v-model.trim="credentials.password" | ||||
|             type="password" | ||||
|             :class="{ error: $v.credentials.password.$error }" | ||||
|             :label="$t('LOGIN.PASSWORD.LABEL')" | ||||
|             :placeholder="$t('SET_NEW_PASSWORD.PASSWORD.PLACEHOLDER')" | ||||
|             :error=" | ||||
|               $v.credentials.email.$error ? $t('REGISTER.EMAIL.ERROR') : '' | ||||
|               $v.credentials.password.$error | ||||
|                 ? $t('SET_NEW_PASSWORD.PASSWORD.ERROR') | ||||
|                 : '' | ||||
|             " | ||||
|             @blur="$v.credentials.email.$touch" | ||||
|             @blur="$v.credentials.password.$touch" | ||||
|           /> | ||||
|  | ||||
|           <woot-input | ||||
|             v-model.trim="credentials.confirmPassword" | ||||
|             type="password" | ||||
|             :class="{ error: $v.credentials.confirmPassword.$error }" | ||||
|             :label="$t('SET_NEW_PASSWORD.CONFIRM_PASSWORD.LABEL')" | ||||
|             :placeholder="$t('SET_NEW_PASSWORD.CONFIRM_PASSWORD.PLACEHOLDER')" | ||||
|             :error=" | ||||
|               $v.credentials.confirmPassword.$error | ||||
|                 ? $t('SET_NEW_PASSWORD.CONFIRM_PASSWORD.ERROR') | ||||
|                 : '' | ||||
|             " | ||||
|             @blur="$v.credentials.confirmPassword.$touch" | ||||
|           /> | ||||
|           <woot-submit-button | ||||
|             :disabled="isSignupInProgress" | ||||
| @@ -89,6 +116,8 @@ export default { | ||||
|         accountName: '', | ||||
|         fullName: '', | ||||
|         email: '', | ||||
|         password: '', | ||||
|         confirmPassword: '', | ||||
|       }, | ||||
|       isSignupInProgress: false, | ||||
|       error: '', | ||||
| @@ -108,6 +137,19 @@ export default { | ||||
|         required, | ||||
|         email, | ||||
|       }, | ||||
|       password: { | ||||
|         required, | ||||
|         minLength: minLength(6), | ||||
|       }, | ||||
|       confirmPassword: { | ||||
|         required, | ||||
|         isEqPassword(value) { | ||||
|           if (value !== this.credentials.password) { | ||||
|             return false; | ||||
|           } | ||||
|           return true; | ||||
|         }, | ||||
|       }, | ||||
|     }, | ||||
|   }, | ||||
|   computed: { | ||||
|   | ||||
| @@ -124,8 +124,8 @@ export default { | ||||
|         this.errorMessage = this.$t('PROFILE_SETTINGS.PASSWORD_UPDATE_SUCCESS'); | ||||
|       } catch (error) { | ||||
|         this.errorMessage = this.$t('RESET_PASSWORD.API.ERROR_MESSAGE'); | ||||
|         if (error?.response?.data?.error) { | ||||
|           this.errorMessage = error.response.data.error; | ||||
|         if (error?.response?.data?.message) { | ||||
|           this.errorMessage = error.response.data.message; | ||||
|         } | ||||
|       } finally { | ||||
|         this.isPasswordChanging = false; | ||||
|   | ||||
| @@ -21,7 +21,5 @@ | ||||
| class SuperAdmin < ApplicationRecord | ||||
|   # Include default devise modules. Others available are: | ||||
|   # :confirmable, :lockable, :timeoutable, :trackable and :omniauthable | ||||
|   devise :database_authenticatable, :trackable, :rememberable, :validatable | ||||
|  | ||||
|   include AccessTokenable | ||||
|   devise :database_authenticatable, :trackable, :rememberable, :validatable, :password_has_required_content | ||||
| end | ||||
|   | ||||
| @@ -53,7 +53,8 @@ class User < ApplicationRecord | ||||
|          :rememberable, | ||||
|          :trackable, | ||||
|          :validatable, | ||||
|          :confirmable | ||||
|          :confirmable, | ||||
|          :password_has_required_content | ||||
|  | ||||
|   enum availability: { online: 0, offline: 1, busy: 2 } | ||||
|  | ||||
|   | ||||
| @@ -4,7 +4,7 @@ | ||||
| # layout will be rendered with erb and other content in html format | ||||
| # Further processing in liquid is implemented in mailers | ||||
|  | ||||
| # Note: rails resolver looks for templates in cache first | ||||
| # NOTE: rails resolver looks for templates in cache first | ||||
| # which we don't want to happen here | ||||
| # so we are overriding find_all method in action view resolver | ||||
| # If anything breaks - look into rails : actionview/lib/action_view/template/resolver.rb | ||||
|   | ||||
| @@ -79,12 +79,12 @@ class Notification::PushNotificationService | ||||
|  | ||||
|   def fcm_options | ||||
|     { | ||||
|       "notification": { | ||||
|         "title": notification.notification_type.titleize, | ||||
|         "body": notification.push_message_title | ||||
|       notification: { | ||||
|         title: notification.notification_type.titleize, | ||||
|         body: notification.push_message_title | ||||
|       }, | ||||
|       "data": { notification: notification.push_event_data.to_json }, | ||||
|       "collapse_key": "chatwoot_#{notification.primary_actor_type.downcase}_#{notification.primary_actor_id}" | ||||
|       data: { notification: notification.push_event_data.to_json }, | ||||
|       collapse_key: "chatwoot_#{notification.primary_actor_type.downcase}_#{notification.primary_actor_id}" | ||||
|     } | ||||
|   end | ||||
| end | ||||
|   | ||||
| @@ -1 +1,3 @@ | ||||
| require Rails.root.join('lib/action_view/template/handlers/liquid') | ||||
|  | ||||
| ActionView::Template.register_template_handler :liquid, ActionView::Template::Handlers::Liquid | ||||
|   | ||||
							
								
								
									
										44
									
								
								config/initializers/secure_password.rb
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										44
									
								
								config/initializers/secure_password.rb
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,44 @@ | ||||
| # frozen_string_literal: true | ||||
|  | ||||
| Devise.setup do |config| | ||||
|   # ==> Configuration for the Devise Secure Password extension | ||||
|   #     Module: password_has_required_content | ||||
|   # | ||||
|   # Configure password content requirements including the number of uppercase, | ||||
|   # lowercase, number, and special characters that are required. To configure the | ||||
|   # minimum and maximum length refer to the Devise config.password_length | ||||
|   # standard configuration parameter. | ||||
|  | ||||
|   # The number of uppercase letters (latin A-Z) required in a password: | ||||
|   config.password_required_uppercase_count = 1 | ||||
|  | ||||
|   # The number of lowercase letters (latin A-Z) required in a password: | ||||
|   config.password_required_lowercase_count = 1 | ||||
|  | ||||
|   # The number of numbers (0-9) required in a password: | ||||
|   config.password_required_number_count = 1 | ||||
|  | ||||
|   # The number of special characters (!@#$%^&*()_+-=[]{}|') required in a password: | ||||
|   config.password_required_special_character_count = 1 | ||||
|  | ||||
|   # we are not using the configurations below | ||||
|   # ==> Configuration for the Devise Secure Password extension | ||||
|   #     Module: password_disallows_frequent_reuse | ||||
|   # | ||||
|   # The number of previously used passwords that can not be reused: | ||||
|   # config.password_previously_used_count = 8 | ||||
|  | ||||
|   # ==> Configuration for the Devise Secure Password extension | ||||
|   #     Module: password_disallows_frequent_changes | ||||
|   #     *Requires* password_disallows_frequent_reuse | ||||
|   # | ||||
|   # The minimum time that must pass between password changes: | ||||
|   # config.password_minimum_age = 1.days | ||||
|  | ||||
|   # ==> Configuration for the Devise Secure Password extension | ||||
|   #     Module: password_requires_regular_updates | ||||
|   #     *Requires* password_disallows_frequent_reuse | ||||
|   # | ||||
|   # The maximum allowed age of a password: | ||||
|   # config.password_maximum_age = 180.days | ||||
| end | ||||
| @@ -1,3 +1,5 @@ | ||||
| require Rails.root.join('lib/redis/config') | ||||
|  | ||||
| schedule_file = 'config/schedule.yml' | ||||
|  | ||||
| Sidekiq.configure_client do |config| | ||||
|   | ||||
							
								
								
									
										74
									
								
								config/locales/secure_password.en.yml
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										74
									
								
								config/locales/secure_password.en.yml
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,74 @@ | ||||
| en: | ||||
|   secure_password: | ||||
|     character: | ||||
|       one: "character" | ||||
|       other: "characters" | ||||
|  | ||||
|     types: | ||||
|       uppercase: "uppercase" | ||||
|       downcase: "downcase" | ||||
|       lowercase: "lowercase" | ||||
|       number: "number" | ||||
|       special: "special" | ||||
|  | ||||
|     password_has_required_content: | ||||
|       errors: | ||||
|         messages: | ||||
|           unknown_characters: "contains %{count} invalid %{subject}" | ||||
|           minimum_characters: "must contain at least %{count} %{type} %{subject}" | ||||
|           maximum_characters: "must contain less than %{count} %{type} %{subject}" | ||||
|           minimum_length: "must contain at least %{count} %{subject}" | ||||
|           maximum_length: "must contain less than %{count} %{subject}" | ||||
|     password_disallows_frequent_reuse: | ||||
|       errors: | ||||
|         messages: | ||||
|           password_is_recent: "Last %{count} passwords may not be reused" | ||||
|     password_disallows_frequent_changes: | ||||
|       errors: | ||||
|         messages: | ||||
|           password_is_recent: "Password cannot be changed more than once per %{timeframe}" | ||||
|     password_requires_regular_updates: | ||||
|       alerts: | ||||
|         messages: | ||||
|           password_updated: "Your password has been updated." | ||||
|       errors: | ||||
|         messages: | ||||
|           password_expired: "Your password has expired. Passwords must be changed every %{timeframe}" | ||||
|     datetime: | ||||
|       # update distance_in_words translations to remove the determiner words: | ||||
|       #   about, almost, over, less than, etc. | ||||
|       precise_distance_in_words: | ||||
|         half_a_minute: "half a minute" | ||||
|         less_than_x_seconds: | ||||
|           one:   "1 second" # default was: "less than 1 second" | ||||
|           other: "%{count} seconds" # default was: "less than %{count} seconds" | ||||
|         x_seconds: | ||||
|           one:   "1 second" | ||||
|           other: "%{count} seconds" | ||||
|         less_than_x_minutes: | ||||
|           one:   "a minute" # default was: "less than a minute" | ||||
|           other: "%{count} minutes" # default was: "less than %{count} minutes" | ||||
|         x_minutes: | ||||
|           one:   "1 minute" | ||||
|           other: "%{count} minutes" | ||||
|         about_x_hours: | ||||
|           one:   "1 hour" # default was: "about 1 hour" | ||||
|           other: "%{count} hours" # default was: "about %{count} hours" | ||||
|         x_days: | ||||
|           one:   "1 day" | ||||
|           other: "%{count} days" | ||||
|         about_x_months: | ||||
|           one:   "1 month" # default was: "about 1 month" | ||||
|           other: "%{count} months" # default was: "about %{count} months" | ||||
|         x_months: | ||||
|           one:   "1 month" | ||||
|           other: "%{count} months" | ||||
|         about_x_years: | ||||
|           one:   "1 year" # default was: "about 1 year" | ||||
|           other: "%{count} years" # default was: "about %{count} years" | ||||
|         over_x_years: | ||||
|           one:   "1 year" # default was: "over 1 year" | ||||
|           other: "%{count} years" # default was: "over %{count} years" | ||||
|         almost_x_years: | ||||
|           one:   "1 year" # default was: "almost 1 year" | ||||
|           other: "%{count} years" # default was: "almost %{count} years" | ||||
| @@ -0,0 +1,5 @@ | ||||
| class RemoveSuperAdminAccessTokes < ActiveRecord::Migration[6.0] | ||||
|   def change | ||||
|     AccessToken.where(owner_type: 'SuperAdmin').destroy_all | ||||
|   end | ||||
| end | ||||
| @@ -10,7 +10,7 @@ | ||||
| # | ||||
| # It's strongly recommended that you check this file into your version control system. | ||||
|  | ||||
| ActiveRecord::Schema.define(version: 2021_05_20_200729) do | ||||
| ActiveRecord::Schema.define(version: 2021_05_27_173755) do | ||||
|  | ||||
|   # These are extensions that must be enabled in order to support this database | ||||
|   enable_extension "pg_stat_statements" | ||||
|   | ||||
| @@ -10,13 +10,13 @@ end | ||||
|  | ||||
| ## Seeds for Local Development | ||||
| unless Rails.env.production? | ||||
|   SuperAdmin.create!(email: 'john@acme.inc', password: '123456') | ||||
|   SuperAdmin.create!(email: 'john@acme.inc', password: 'Password1!') | ||||
|  | ||||
|   account = Account.create!( | ||||
|     name: 'Acme Inc' | ||||
|   ) | ||||
|  | ||||
|   user = User.new(name: 'John', email: 'john@acme.inc', password: '123456') | ||||
|   user = User.new(name: 'John', email: 'john@acme.inc', password: 'Password1!') | ||||
|   user.skip_confirmation! | ||||
|   user.save! | ||||
|  | ||||
|   | ||||
| @@ -29,19 +29,19 @@ module WootMessageSeeder | ||||
|  | ||||
|   def self.sample_card_item | ||||
|     { | ||||
|       "media_url": 'https://i.imgur.com/d8Djr4k.jpg', | ||||
|       "title": 'Acme Shoes 2.0', | ||||
|       "description": 'Move with Acme Shoe 2.0', | ||||
|       "actions": [ | ||||
|       media_url: 'https://i.imgur.com/d8Djr4k.jpg', | ||||
|       title: 'Acme Shoes 2.0', | ||||
|       description: 'Move with Acme Shoe 2.0', | ||||
|       actions: [ | ||||
|         { | ||||
|           "type": 'link', | ||||
|           "text": 'View More', | ||||
|           "uri": 'http://acme-shoes.inc' | ||||
|           type: 'link', | ||||
|           text: 'View More', | ||||
|           uri: 'http://acme-shoes.inc' | ||||
|         }, | ||||
|         { | ||||
|           "type": 'postback', | ||||
|           "text": 'Add to cart', | ||||
|           "payload": 'ITEM_SELECTED' | ||||
|           type: 'postback', | ||||
|           text: 'Add to cart', | ||||
|           payload: 'ITEM_SELECTED' | ||||
|         } | ||||
|       ] | ||||
|     } | ||||
| @@ -56,11 +56,11 @@ module WootMessageSeeder | ||||
|       content: 'Your favorite food', | ||||
|       content_type: 'input_select', | ||||
|       content_attributes: { | ||||
|         "items": [ | ||||
|           { "title": '🌯 Burito', "value": 'Burito' }, | ||||
|           { "title": '🍝 Pasta', "value": 'Pasta' }, | ||||
|           { "title": ' 🍱 Sushi', "value": 'Sushi' }, | ||||
|           { "title": ' 🥗 Salad', "value": 'Salad' } | ||||
|         items: [ | ||||
|           { title: '🌯 Burito', value: 'Burito' }, | ||||
|           { title: '🍝 Pasta', value: 'Pasta' }, | ||||
|           { title: ' 🍱 Sushi', value: 'Sushi' }, | ||||
|           { title: ' 🥗 Salad', value: 'Salad' } | ||||
|         ] | ||||
|       } | ||||
|     ) | ||||
| @@ -75,12 +75,12 @@ module WootMessageSeeder | ||||
|       content_type: 'form', | ||||
|       content: 'form', | ||||
|       content_attributes: { | ||||
|         "items": [ | ||||
|           { "name": 'email', "placeholder": 'Please enter your email', "type": 'email', "label": 'Email' }, | ||||
|           { "name": 'text_area', "placeholder": 'Please enter text', "type": 'text_area', "label": 'Large Text' }, | ||||
|           { "name": 'text', "placeholder": 'Please enter text', "type": 'text', "label": 'text', "default": 'defaut value' }, | ||||
|           { "name": 'select', "label": 'Select Option', "type": 'select', "options": [{ "label": '🌯 Burito', "value": 'Burito' }, | ||||
|                                                                                       { "label": '🍝 Pasta', "value": 'Pasta' }] } | ||||
|         items: [ | ||||
|           { name: 'email', placeholder: 'Please enter your email', type: 'email', label: 'Email' }, | ||||
|           { name: 'text_area', placeholder: 'Please enter text', type: 'text_area', label: 'Large Text' }, | ||||
|           { name: 'text', placeholder: 'Please enter text', type: 'text', label: 'text', default: 'defaut value' }, | ||||
|           { name: 'select', label: 'Select Option', type: 'select', options: [{ label: '🌯 Burito', value: 'Burito' }, | ||||
|                                                                               { label: '🍝 Pasta', value: 'Pasta' }] } | ||||
|         ] | ||||
|       } | ||||
|     ) | ||||
| @@ -95,9 +95,9 @@ module WootMessageSeeder | ||||
|       content: 'Tech Companies', | ||||
|       content_type: 'article', | ||||
|       content_attributes: { | ||||
|         "items": [ | ||||
|           { "title": 'Acme Hardware', "description": 'Hardware reimagined', "link": 'http://acme-hardware.inc' }, | ||||
|           { "title": 'Acme Search', "description": 'The best Search Engine', "link": 'http://acme-search.inc' } | ||||
|         items: [ | ||||
|           { title: 'Acme Hardware', description: 'Hardware reimagined', link: 'http://acme-hardware.inc' }, | ||||
|           { title: 'Acme Search', description: 'The best Search Engine', link: 'http://acme-search.inc' } | ||||
|         ] | ||||
|       } | ||||
|     ) | ||||
|   | ||||
| @@ -18,13 +18,13 @@ RSpec.describe 'Accounts API', type: :request do | ||||
|       it 'calls account builder' do | ||||
|         allow(account_builder).to receive(:perform).and_return([user, account]) | ||||
|  | ||||
|         params = { account_name: 'test', email: email, user: nil, user_full_name: user_full_name } | ||||
|         params = { account_name: 'test', email: email, user: nil, user_full_name: user_full_name, password: 'Password1!' } | ||||
|  | ||||
|         post api_v1_accounts_url, | ||||
|              params: params, | ||||
|              as: :json | ||||
|  | ||||
|         expect(AccountBuilder).to have_received(:new).with(params.merge(confirmed: false)) | ||||
|         expect(AccountBuilder).to have_received(:new).with(params.except(:password).merge(user_password: params[:password])) | ||||
|         expect(account_builder).to have_received(:perform) | ||||
|         expect(response.headers.keys).to include('access-token', 'token-type', 'client', 'expiry', 'uid') | ||||
|       end | ||||
| @@ -38,44 +38,11 @@ RSpec.describe 'Accounts API', type: :request do | ||||
|              params: params, | ||||
|              as: :json | ||||
|  | ||||
|         expect(AccountBuilder).to have_received(:new).with(params.merge(confirmed: false)) | ||||
|         expect(AccountBuilder).to have_received(:new).with(params.merge(user_password: params[:password])) | ||||
|         expect(account_builder).to have_received(:perform) | ||||
|         expect(response).to have_http_status(:forbidden) | ||||
|         expect(response.body).to eq({ message: I18n.t('errors.signup.failed') }.to_json) | ||||
|       end | ||||
|  | ||||
|       it 'ignores confirmed param when called with out super admin token' do | ||||
|         allow(account_builder).to receive(:perform).and_return(nil) | ||||
|  | ||||
|         params = { account_name: 'test', email: email, confirmed: true, user: nil, user_full_name: user_full_name } | ||||
|  | ||||
|         post api_v1_accounts_url, | ||||
|              params: params, | ||||
|              as: :json | ||||
|  | ||||
|         expect(AccountBuilder).to have_received(:new).with(params.merge(confirmed: false)) | ||||
|         expect(account_builder).to have_received(:perform) | ||||
|         expect(response).to have_http_status(:forbidden) | ||||
|         expect(response.body).to eq({ message: I18n.t('errors.signup.failed') }.to_json) | ||||
|       end | ||||
|     end | ||||
|  | ||||
|     context 'when called with super admin token' do | ||||
|       let(:super_admin) { create(:super_admin) } | ||||
|  | ||||
|       it 'calls account builder with confirmed true when confirmed param is passed' do | ||||
|         params = { account_name: 'test', email: email, confirmed: true, user_full_name: user_full_name } | ||||
|  | ||||
|         post api_v1_accounts_url, | ||||
|              params: params, | ||||
|              headers: { api_access_token: super_admin.access_token.token }, | ||||
|              as: :json | ||||
|  | ||||
|         created_user = User.find_by(email: email) | ||||
|         expect(created_user.confirmed?).to eq(true) | ||||
|         expect(response.headers.keys).to include('access-token', 'token-type', 'client', 'expiry', 'uid') | ||||
|         expect(response.body).to include(created_user.access_token.token) | ||||
|       end | ||||
|     end | ||||
|  | ||||
|     context 'when ENABLE_ACCOUNT_SIGNUP env variable is set to false' do | ||||
|   | ||||
| @@ -44,7 +44,7 @@ RSpec.describe 'Profile API', type: :request do | ||||
|       it 'updates the name & email' do | ||||
|         new_email = Faker::Internet.email | ||||
|         put '/api/v1/profile', | ||||
|             params: { profile: { name: 'test', 'email': new_email } }, | ||||
|             params: { profile: { name: 'test', email: new_email } }, | ||||
|             headers: agent.create_new_auth_token, | ||||
|             as: :json | ||||
|  | ||||
|   | ||||
| @@ -18,12 +18,8 @@ RSpec.describe 'Token Confirmation', type: :request do | ||||
|         expect(response.status).to eq 200 | ||||
|       end | ||||
|  | ||||
|       it 'returns message "Success"' do | ||||
|         expect(response_json[:message]).to eq 'Success' | ||||
|       end | ||||
|  | ||||
|       it 'returns "redirect_url"' do | ||||
|         expect(response_json[:redirect_url]).to include '/app/auth/password/edit?config=default&redirect_url=&reset_password_token' | ||||
|       it 'returns "auth data"' do | ||||
|         expect(response.body).to include('john.doe@gmail.com') | ||||
|       end | ||||
|     end | ||||
|  | ||||
|   | ||||
| @@ -17,10 +17,10 @@ RSpec.describe 'Session', type: :request do | ||||
|     end | ||||
|  | ||||
|     context 'when it is valid credentials' do | ||||
|       let!(:user) { create(:user, password: 'test1234', account: account) } | ||||
|       let!(:user) { create(:user, password: 'Password1!', account: account) } | ||||
|  | ||||
|       it 'returns successful auth response' do | ||||
|         params = { email: user.email, password: 'test1234' } | ||||
|         params = { email: user.email, password: 'Password1!' } | ||||
|  | ||||
|         post new_user_session_url, | ||||
|              params: params, | ||||
| @@ -32,7 +32,7 @@ RSpec.describe 'Session', type: :request do | ||||
|     end | ||||
|  | ||||
|     context 'when it is invalid sso auth token' do | ||||
|       let!(:user) { create(:user, password: 'test1234', account: account) } | ||||
|       let!(:user) { create(:user, password: 'Password1!', account: account) } | ||||
|  | ||||
|       it 'returns unauthorized' do | ||||
|         params = { email: user.email, sso_auth_token: SecureRandom.hex(32) } | ||||
| @@ -46,7 +46,7 @@ RSpec.describe 'Session', type: :request do | ||||
|     end | ||||
|  | ||||
|     context 'when with valid sso auth token' do | ||||
|       let!(:user) { create(:user, password: 'test1234', account: account) } | ||||
|       let!(:user) { create(:user, password: 'Password1!', account: account) } | ||||
|  | ||||
|       it 'returns successful auth response' do | ||||
|         params = { email: user.email, sso_auth_token: user.generate_sso_auth_token } | ||||
|   | ||||
| @@ -94,7 +94,7 @@ RSpec.describe 'Platform Users API', type: :request do | ||||
|       let(:platform_app) { create(:platform_app) } | ||||
|  | ||||
|       it 'creates a new user and permissible for the user' do | ||||
|         post '/platform/api/v1/users/', params: { name: 'test', email: 'test@test.com', password: 'password123' }, | ||||
|         post '/platform/api/v1/users/', params: { name: 'test', email: 'test@test.com', password: 'Password1!' }, | ||||
|                                         headers: { api_access_token: platform_app.access_token.token }, as: :json | ||||
|  | ||||
|         expect(response).to have_http_status(:success) | ||||
| @@ -105,7 +105,7 @@ RSpec.describe 'Platform Users API', type: :request do | ||||
|  | ||||
|       it 'fetch existing user and creates permissible for the user' do | ||||
|         create(:user, name: 'old test', email: 'test@test.com') | ||||
|         post '/platform/api/v1/users/', params: { name: 'test', email: 'test@test.com', password: 'password123' }, | ||||
|         post '/platform/api/v1/users/', params: { name: 'test', email: 'test@test.com', password: 'Password1!' }, | ||||
|                                         headers: { api_access_token: platform_app.access_token.token }, as: :json | ||||
|  | ||||
|         expect(response).to have_http_status(:success) | ||||
|   | ||||
| @@ -2,6 +2,7 @@ require 'rails_helper' | ||||
|  | ||||
| RSpec.describe 'Super Admin access tokens API', type: :request do | ||||
|   let(:super_admin) { create(:super_admin) } | ||||
|   let!(:platform_app) { create(:platform_app) } | ||||
|  | ||||
|   describe 'GET /super_admin/access_tokens' do | ||||
|     context 'when it is an unauthenticated super admin' do | ||||
| @@ -16,7 +17,7 @@ RSpec.describe 'Super Admin access tokens API', type: :request do | ||||
|         sign_in super_admin | ||||
|         get '/super_admin/access_tokens' | ||||
|         expect(response).to have_http_status(:success) | ||||
|         expect(response.body).to include(super_admin.access_token.token) | ||||
|         expect(response.body).to include(platform_app.access_token.token) | ||||
|       end | ||||
|     end | ||||
|   end | ||||
|   | ||||
| @@ -1,6 +1,6 @@ | ||||
| FactoryBot.define do | ||||
|   factory :super_admin do | ||||
|     email { "admin@#{SecureRandom.uuid}.com" } | ||||
|     password { 'password' } | ||||
|     password { 'Password1!' } | ||||
|   end | ||||
| end | ||||
|   | ||||
| @@ -14,7 +14,7 @@ FactoryBot.define do | ||||
|     name { Faker::Name.name } | ||||
|     display_name { Faker::Name.first_name } | ||||
|     email { display_name + "@#{SecureRandom.uuid}.com" } | ||||
|     password { 'password' } | ||||
|     password { 'Password1!' } | ||||
|  | ||||
|     after(:build) do |user, evaluator| | ||||
|       user.skip_confirmation! if evaluator.skip_confirmation | ||||
|   | ||||
| @@ -3,7 +3,7 @@ require 'rails_helper' | ||||
| describe ChatwootHub do | ||||
|   it 'get latest version from chatwoot hub' do | ||||
|     version = '1.1.1' | ||||
|     allow(RestClient).to receive(:get).and_return({ 'version': version }.to_json) | ||||
|     allow(RestClient).to receive(:get).and_return({ version: version }.to_json) | ||||
|     expect(described_class.latest_version).to eq version | ||||
|     expect(RestClient).to have_received(:get).with(described_class::BASE_URL, { params: described_class.instance_config }) | ||||
|   end | ||||
|   | ||||
		Reference in New Issue
	
	Block a user
	 Sojan Jose
					Sojan Jose