enhancement: Current password confirmation in change password (#2108)
* add current password field in change password form * locale changes * chore: update password API * chore: rubocop fixes * replace currentPassword with current_password * code cleanup * replace input with woot-input * code cleanup Co-authored-by: Sojan <sojan@pepalo.com>
This commit is contained in:
parent
2c42e70637
commit
b0b4d9d6f5
6 changed files with 201 additions and 84 deletions
|
@ -6,6 +6,12 @@ class Api::V1::ProfilesController < Api::BaseController
|
|||
end
|
||||
|
||||
def update
|
||||
if password_params[:password].present?
|
||||
render_could_not_create_error('Invalid current password') and return unless @user.valid_password?(password_params[:current_password])
|
||||
|
||||
@user.update!(password_params.except(:current_password))
|
||||
end
|
||||
|
||||
@user.update!(profile_params)
|
||||
end
|
||||
|
||||
|
@ -20,11 +26,17 @@ class Api::V1::ProfilesController < Api::BaseController
|
|||
:email,
|
||||
:name,
|
||||
:display_name,
|
||||
:password,
|
||||
:password_confirmation,
|
||||
:avatar,
|
||||
:availability,
|
||||
ui_settings: {}
|
||||
)
|
||||
end
|
||||
|
||||
def password_params
|
||||
params.require(:profile).permit(
|
||||
:current_password,
|
||||
:password,
|
||||
:password_confirmation
|
||||
)
|
||||
end
|
||||
end
|
||||
|
|
|
@ -74,6 +74,11 @@
|
|||
"ERROR": "Please enter a valid email address",
|
||||
"PLACEHOLDER": "Please enter your email address, this would be displayed in conversations"
|
||||
},
|
||||
"CURRENT_PASSWORD": {
|
||||
"LABEL": "Current password",
|
||||
"ERROR": "Please enter the current password",
|
||||
"PLACEHOLDER": "Please enter the current password"
|
||||
},
|
||||
"PASSWORD": {
|
||||
"LABEL": "Password",
|
||||
"ERROR": "Please enter a password of length 6 or more",
|
||||
|
|
|
@ -0,0 +1,152 @@
|
|||
<template>
|
||||
<form @submit.prevent="changePassword()">
|
||||
<div class="profile--settings--row row">
|
||||
<div class="columns small-3">
|
||||
<h4 class="block-title">
|
||||
{{ $t('PROFILE_SETTINGS.FORM.PASSWORD_SECTION.TITLE') }}
|
||||
</h4>
|
||||
<p>{{ $t('PROFILE_SETTINGS.FORM.PASSWORD_SECTION.NOTE') }}</p>
|
||||
</div>
|
||||
<div class="columns small-9 medium-5">
|
||||
<woot-input
|
||||
v-model="currentPassword"
|
||||
type="password"
|
||||
:class="{ error: $v.currentPassword.$error }"
|
||||
:label="$t('PROFILE_SETTINGS.FORM.CURRENT_PASSWORD.LABEL')"
|
||||
:placeholder="
|
||||
$t('PROFILE_SETTINGS.FORM.CURRENT_PASSWORD.PLACEHOLDER')
|
||||
"
|
||||
:error="
|
||||
$v.currentPassword.$error
|
||||
? $t('PROFILE_SETTINGS.FORM.CURRENT_PASSWORD.ERROR')
|
||||
: ''
|
||||
"
|
||||
@blur="$v.currentPassword.$touch"
|
||||
/>
|
||||
|
||||
<woot-input
|
||||
v-model="password"
|
||||
type="password"
|
||||
:class="{ error: $v.password.$error }"
|
||||
:label="$t('PROFILE_SETTINGS.FORM.PASSWORD.LABEL')"
|
||||
:placeholder="$t('PROFILE_SETTINGS.FORM.PASSWORD.PLACEHOLDER')"
|
||||
:error="
|
||||
$v.password.$error ? $t('PROFILE_SETTINGS.FORM.PASSWORD.ERROR') : ''
|
||||
"
|
||||
@blur="$v.password.$touch"
|
||||
/>
|
||||
|
||||
<woot-input
|
||||
v-model="passwordConfirmation"
|
||||
type="password"
|
||||
:class="{ error: $v.passwordConfirmation.$error }"
|
||||
:label="$t('PROFILE_SETTINGS.FORM.PASSWORD_CONFIRMATION.LABEL')"
|
||||
:placeholder="
|
||||
$t('PROFILE_SETTINGS.FORM.PASSWORD_CONFIRMATION.PLACEHOLDER')
|
||||
"
|
||||
:error="
|
||||
$v.passwordConfirmation.$error
|
||||
? $t('PROFILE_SETTINGS.FORM.PASSWORD_CONFIRMATION.ERROR')
|
||||
: ''
|
||||
"
|
||||
@blur="$v.passwordConfirmation.$touch"
|
||||
/>
|
||||
|
||||
<woot-button
|
||||
:is-loading="isPasswordChanging"
|
||||
type="submit"
|
||||
:disabled="
|
||||
!currentPassword ||
|
||||
!passwordConfirmation ||
|
||||
!$v.passwordConfirmation.isEqPassword
|
||||
"
|
||||
>
|
||||
{{ $t('PROFILE_SETTINGS.FORM.PASSWORD_SECTION.BTN_TEXT') }}
|
||||
</woot-button>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { required, minLength } from 'vuelidate/lib/validators';
|
||||
import { mapGetters } from 'vuex';
|
||||
import alertMixin from 'shared/mixins/alertMixin';
|
||||
|
||||
export default {
|
||||
mixins: [alertMixin],
|
||||
data() {
|
||||
return {
|
||||
currentPassword: '',
|
||||
password: '',
|
||||
passwordConfirmation: '',
|
||||
isPasswordChanging: false,
|
||||
errorMessage: '',
|
||||
};
|
||||
},
|
||||
validations: {
|
||||
currentPassword: {
|
||||
required,
|
||||
},
|
||||
password: {
|
||||
minLength: minLength(6),
|
||||
},
|
||||
passwordConfirmation: {
|
||||
minLength: minLength(6),
|
||||
isEqPassword(value) {
|
||||
if (value !== this.password) {
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
},
|
||||
},
|
||||
},
|
||||
computed: {
|
||||
...mapGetters({
|
||||
currentUser: 'getCurrentUser',
|
||||
currentUserId: 'getCurrentUserID',
|
||||
}),
|
||||
},
|
||||
methods: {
|
||||
async changePassword() {
|
||||
this.$v.$touch();
|
||||
if (this.$v.$invalid) {
|
||||
this.showAlert(this.$t('PROFILE_SETTINGS.FORM.ERROR'));
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
await this.$store.dispatch('updateProfile', {
|
||||
password: this.password,
|
||||
password_confirmation: this.passwordConfirmation,
|
||||
current_password: this.currentPassword,
|
||||
});
|
||||
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;
|
||||
}
|
||||
} finally {
|
||||
this.isPasswordChanging = false;
|
||||
this.showAlert(this.errorMessage);
|
||||
}
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
<style lang="scss">
|
||||
@import '~dashboard/assets/scss/mixins.scss';
|
||||
|
||||
.profile--settings--row {
|
||||
@include border-normal-bottom;
|
||||
padding: var(--space-normal);
|
||||
.small-3 {
|
||||
padding: var(--space-normal) var(--space-medium) var(--space-normal) 0;
|
||||
}
|
||||
.small-9 {
|
||||
padding: var(--space-normal);
|
||||
}
|
||||
}
|
||||
</style>
|
|
@ -55,53 +55,7 @@
|
|||
</div>
|
||||
</div>
|
||||
</form>
|
||||
<form @submit.prevent="updateUser('password')">
|
||||
<div class="profile--settings--row row">
|
||||
<div class="columns small-3">
|
||||
<h4 class="block-title">
|
||||
{{ $t('PROFILE_SETTINGS.FORM.PASSWORD_SECTION.TITLE') }}
|
||||
</h4>
|
||||
<p>{{ $t('PROFILE_SETTINGS.FORM.PASSWORD_SECTION.NOTE') }}</p>
|
||||
</div>
|
||||
<div class="columns small-9 medium-5">
|
||||
<label :class="{ error: $v.password.$error }">
|
||||
{{ $t('PROFILE_SETTINGS.FORM.PASSWORD.LABEL') }}
|
||||
<input
|
||||
v-model.trim="password"
|
||||
type="password"
|
||||
:placeholder="$t('PROFILE_SETTINGS.FORM.PASSWORD.PLACEHOLDER')"
|
||||
@input="$v.password.$touch"
|
||||
/>
|
||||
<span v-if="$v.password.$error" class="message">
|
||||
{{ $t('PROFILE_SETTINGS.FORM.PASSWORD.ERROR') }}
|
||||
</span>
|
||||
</label>
|
||||
<label :class="{ error: $v.passwordConfirmation.$error }">
|
||||
{{ $t('PROFILE_SETTINGS.FORM.PASSWORD_CONFIRMATION.LABEL') }}
|
||||
<input
|
||||
v-model.trim="passwordConfirmation"
|
||||
type="password"
|
||||
:placeholder="
|
||||
$t('PROFILE_SETTINGS.FORM.PASSWORD_CONFIRMATION.PLACEHOLDER')
|
||||
"
|
||||
@input="$v.passwordConfirmation.$touch"
|
||||
/>
|
||||
<span v-if="$v.passwordConfirmation.$error" class="message">
|
||||
{{ $t('PROFILE_SETTINGS.FORM.PASSWORD_CONFIRMATION.ERROR') }}
|
||||
</span>
|
||||
</label>
|
||||
<woot-button
|
||||
:is-loading="isPasswordChanging"
|
||||
type="submit"
|
||||
:disabled="
|
||||
!passwordConfirmation || !$v.passwordConfirmation.isEqPassword
|
||||
"
|
||||
>
|
||||
{{ $t('PROFILE_SETTINGS.FORM.PASSWORD_SECTION.BTN_TEXT') }}
|
||||
</woot-button>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
<change-password />
|
||||
<notification-settings />
|
||||
<div class="profile--settings--row row">
|
||||
<div class="columns small-3">
|
||||
|
@ -123,10 +77,12 @@ import { mapGetters } from 'vuex';
|
|||
import { clearCookiesOnLogout } from '../../../../store/utils/api';
|
||||
import NotificationSettings from './NotificationSettings';
|
||||
import alertMixin from 'shared/mixins/alertMixin';
|
||||
import ChangePassword from './ChangePassword.vue';
|
||||
|
||||
export default {
|
||||
components: {
|
||||
NotificationSettings,
|
||||
ChangePassword,
|
||||
},
|
||||
mixins: [alertMixin],
|
||||
data() {
|
||||
|
@ -136,10 +92,8 @@ export default {
|
|||
name: '',
|
||||
displayName: '',
|
||||
email: '',
|
||||
password: '',
|
||||
passwordConfirmation: '',
|
||||
isProfileUpdating: false,
|
||||
isPasswordChanging: false,
|
||||
errorMessage: '',
|
||||
};
|
||||
},
|
||||
validations: {
|
||||
|
@ -152,18 +106,6 @@ export default {
|
|||
required,
|
||||
email,
|
||||
},
|
||||
password: {
|
||||
minLength: minLength(6),
|
||||
},
|
||||
passwordConfirmation: {
|
||||
minLength: minLength(6),
|
||||
isEqPassword(value) {
|
||||
if (value !== this.password) {
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
},
|
||||
},
|
||||
},
|
||||
computed: {
|
||||
...mapGetters({
|
||||
|
@ -190,41 +132,36 @@ export default {
|
|||
this.avatarUrl = this.currentUser.avatar_url;
|
||||
this.displayName = this.currentUser.display_name;
|
||||
},
|
||||
async updateUser(type) {
|
||||
async updateUser() {
|
||||
this.$v.$touch();
|
||||
if (this.$v.$invalid) {
|
||||
this.showAlert(this.$t('PROFILE_SETTINGS.FORM.ERROR'));
|
||||
return;
|
||||
}
|
||||
if (type === 'profile') {
|
||||
|
||||
this.isProfileUpdating = true;
|
||||
} else if (type === 'password') {
|
||||
this.isPasswordChanging = true;
|
||||
}
|
||||
const hasEmailChanged = this.currentUser.email !== this.email;
|
||||
try {
|
||||
await this.$store.dispatch('updateProfile', {
|
||||
name: this.name,
|
||||
email: this.email,
|
||||
avatar: this.avatarFile,
|
||||
password: this.password,
|
||||
displayName: this.displayName,
|
||||
password_confirmation: this.passwordConfirmation,
|
||||
});
|
||||
this.isProfileUpdating = false;
|
||||
this.isPasswordChanging = false;
|
||||
if (hasEmailChanged) {
|
||||
clearCookiesOnLogout();
|
||||
this.showAlert(this.$t('PROFILE_SETTINGS.AFTER_EMAIL_CHANGED'));
|
||||
}
|
||||
if (type === 'profile') {
|
||||
this.showAlert(this.$t('PROFILE_SETTINGS.UPDATE_SUCCESS'));
|
||||
} else if (type === 'password') {
|
||||
this.showAlert(this.$t('PROFILE_SETTINGS.PASSWORD_UPDATE_SUCCESS'));
|
||||
this.errorMessage = this.$t('PROFILE_SETTINGS.AFTER_EMAIL_CHANGED');
|
||||
}
|
||||
this.errorMessage = this.$t('PROFILE_SETTINGS.UPDATE_SUCCESS');
|
||||
} catch (error) {
|
||||
this.errorMessage = this.$t('RESET_PASSWORD.API.ERROR_MESSAGE');
|
||||
if (error?.response?.data?.error) {
|
||||
this.errorMessage = error.response.data.error;
|
||||
}
|
||||
} finally {
|
||||
this.isProfileUpdating = false;
|
||||
this.isPasswordChanging = false;
|
||||
this.showAlert(this.errorMessage);
|
||||
}
|
||||
},
|
||||
handleImageUpload({ file, url }) {
|
||||
|
|
|
@ -102,12 +102,13 @@ export const actions = {
|
|||
},
|
||||
|
||||
updateProfile: async ({ commit }, params) => {
|
||||
// eslint-disable-next-line no-useless-catch
|
||||
try {
|
||||
const response = await authAPI.profileUpdate(params);
|
||||
setUser(response.data, getHeaderExpiry(response));
|
||||
commit(types.default.SET_CURRENT_USER);
|
||||
} catch (error) {
|
||||
// Ignore error
|
||||
throw error;
|
||||
}
|
||||
},
|
||||
|
||||
|
|
|
@ -39,7 +39,7 @@ RSpec.describe 'Profile API', type: :request do
|
|||
end
|
||||
|
||||
context 'when it is an authenticated user' do
|
||||
let(:agent) { create(:user, account: account, role: :agent) }
|
||||
let(:agent) { create(:user, password: 'Test123!', account: account, role: :agent) }
|
||||
|
||||
it 'updates the name & email' do
|
||||
new_email = Faker::Internet.email
|
||||
|
@ -56,13 +56,23 @@ RSpec.describe 'Profile API', type: :request do
|
|||
expect(agent.email).to eq(new_email)
|
||||
end
|
||||
|
||||
it 'updates the password' do
|
||||
it 'updates the password when current password is provided' do
|
||||
put '/api/v1/profile',
|
||||
params: { profile: { password: 'test123', password_confirmation: 'test123' } },
|
||||
params: { profile: { current_password: 'Test123!', password: 'test123', password_confirmation: 'test123' } },
|
||||
headers: agent.create_new_auth_token,
|
||||
as: :json
|
||||
|
||||
expect(response).to have_http_status(:success)
|
||||
expect(agent.reload.valid_password?('test123')).to eq true
|
||||
end
|
||||
|
||||
it 'throws error when current password provided is invalid' do
|
||||
put '/api/v1/profile',
|
||||
params: { profile: { current_password: 'Test', password: 'test123', password_confirmation: 'test123' } },
|
||||
headers: agent.create_new_auth_token,
|
||||
as: :json
|
||||
|
||||
expect(response).to have_http_status(:unprocessable_entity)
|
||||
end
|
||||
|
||||
it 'updates avatar' do
|
||||
|
|
Loading…
Reference in a new issue