feat: Improved password security policy (#2345)

Co-authored-by: Pranav Raj S <pranav@chatwoot.com>
This commit is contained in:
Sojan Jose 2021-06-07 17:26:08 +05:30 committed by GitHub
parent d1b3c7b0c2
commit 467b45b427
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
36 changed files with 284 additions and 151 deletions

View file

@ -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'

View file

@ -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

View file

@ -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!

View file

@ -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

View file

@ -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

View file

@ -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

View file

@ -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')

View file

@ -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

View file

@ -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

View file

@ -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

View file

@ -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);
});
});
},

View file

@ -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;
}

View file

@ -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);
});
},
},

View file

@ -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: {

View file

@ -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;

View file

@ -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

View file

@ -53,7 +53,8 @@ class User < ApplicationRecord
:rememberable,
:trackable,
:validatable,
:confirmable
:confirmable,
:password_has_required_content
enum availability: { online: 0, offline: 1, busy: 2 }

View file

@ -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

View file

@ -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

View file

@ -1 +1,3 @@
require Rails.root.join('lib/action_view/template/handlers/liquid')
ActionView::Template.register_template_handler :liquid, ActionView::Template::Handlers::Liquid

View 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

View file

@ -1,3 +1,5 @@
require Rails.root.join('lib/redis/config')
schedule_file = 'config/schedule.yml'
Sidekiq.configure_client do |config|

View 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"

View file

@ -0,0 +1,5 @@
class RemoveSuperAdminAccessTokes < ActiveRecord::Migration[6.0]
def change
AccessToken.where(owner_type: 'SuperAdmin').destroy_all
end
end

View file

@ -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"

View file

@ -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!

View file

@ -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' }
]
}
)

View file

@ -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

View file

@ -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

View file

@ -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

View file

@ -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 }

View file

@ -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)

View file

@ -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

View file

@ -1,6 +1,6 @@
FactoryBot.define do
factory :super_admin do
email { "admin@#{SecureRandom.uuid}.com" }
password { 'password' }
password { 'Password1!' }
end
end

View file

@ -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

View file

@ -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