feat: Improved password security policy (#2345)
Co-authored-by: Pranav Raj S <pranav@chatwoot.com>
This commit is contained in:
parent
d1b3c7b0c2
commit
467b45b427
36 changed files with 284 additions and 151 deletions
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
|
||||
|
|
Loading…
Reference in a new issue