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:
@@ -6,6 +6,12 @@ class Api::V1::ProfilesController < Api::BaseController
|
|||||||
end
|
end
|
||||||
|
|
||||||
def update
|
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)
|
@user.update!(profile_params)
|
||||||
end
|
end
|
||||||
|
|
||||||
@@ -20,11 +26,17 @@ class Api::V1::ProfilesController < Api::BaseController
|
|||||||
:email,
|
:email,
|
||||||
:name,
|
:name,
|
||||||
:display_name,
|
:display_name,
|
||||||
:password,
|
|
||||||
:password_confirmation,
|
|
||||||
:avatar,
|
:avatar,
|
||||||
:availability,
|
:availability,
|
||||||
ui_settings: {}
|
ui_settings: {}
|
||||||
)
|
)
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def password_params
|
||||||
|
params.require(:profile).permit(
|
||||||
|
:current_password,
|
||||||
|
:password,
|
||||||
|
:password_confirmation
|
||||||
|
)
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|||||||
@@ -74,6 +74,11 @@
|
|||||||
"ERROR": "Please enter a valid email address",
|
"ERROR": "Please enter a valid email address",
|
||||||
"PLACEHOLDER": "Please enter your email address, this would be displayed in conversations"
|
"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": {
|
"PASSWORD": {
|
||||||
"LABEL": "Password",
|
"LABEL": "Password",
|
||||||
"ERROR": "Please enter a password of length 6 or more",
|
"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>
|
||||||
</div>
|
</div>
|
||||||
</form>
|
</form>
|
||||||
<form @submit.prevent="updateUser('password')">
|
<change-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>
|
|
||||||
<notification-settings />
|
<notification-settings />
|
||||||
<div class="profile--settings--row row">
|
<div class="profile--settings--row row">
|
||||||
<div class="columns small-3">
|
<div class="columns small-3">
|
||||||
@@ -123,10 +77,12 @@ import { mapGetters } from 'vuex';
|
|||||||
import { clearCookiesOnLogout } from '../../../../store/utils/api';
|
import { clearCookiesOnLogout } from '../../../../store/utils/api';
|
||||||
import NotificationSettings from './NotificationSettings';
|
import NotificationSettings from './NotificationSettings';
|
||||||
import alertMixin from 'shared/mixins/alertMixin';
|
import alertMixin from 'shared/mixins/alertMixin';
|
||||||
|
import ChangePassword from './ChangePassword.vue';
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
components: {
|
components: {
|
||||||
NotificationSettings,
|
NotificationSettings,
|
||||||
|
ChangePassword,
|
||||||
},
|
},
|
||||||
mixins: [alertMixin],
|
mixins: [alertMixin],
|
||||||
data() {
|
data() {
|
||||||
@@ -136,10 +92,8 @@ export default {
|
|||||||
name: '',
|
name: '',
|
||||||
displayName: '',
|
displayName: '',
|
||||||
email: '',
|
email: '',
|
||||||
password: '',
|
|
||||||
passwordConfirmation: '',
|
|
||||||
isProfileUpdating: false,
|
isProfileUpdating: false,
|
||||||
isPasswordChanging: false,
|
errorMessage: '',
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
validations: {
|
validations: {
|
||||||
@@ -152,18 +106,6 @@ export default {
|
|||||||
required,
|
required,
|
||||||
email,
|
email,
|
||||||
},
|
},
|
||||||
password: {
|
|
||||||
minLength: minLength(6),
|
|
||||||
},
|
|
||||||
passwordConfirmation: {
|
|
||||||
minLength: minLength(6),
|
|
||||||
isEqPassword(value) {
|
|
||||||
if (value !== this.password) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
return true;
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
},
|
||||||
computed: {
|
computed: {
|
||||||
...mapGetters({
|
...mapGetters({
|
||||||
@@ -190,41 +132,36 @@ export default {
|
|||||||
this.avatarUrl = this.currentUser.avatar_url;
|
this.avatarUrl = this.currentUser.avatar_url;
|
||||||
this.displayName = this.currentUser.display_name;
|
this.displayName = this.currentUser.display_name;
|
||||||
},
|
},
|
||||||
async updateUser(type) {
|
async updateUser() {
|
||||||
this.$v.$touch();
|
this.$v.$touch();
|
||||||
if (this.$v.$invalid) {
|
if (this.$v.$invalid) {
|
||||||
this.showAlert(this.$t('PROFILE_SETTINGS.FORM.ERROR'));
|
this.showAlert(this.$t('PROFILE_SETTINGS.FORM.ERROR'));
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (type === 'profile') {
|
|
||||||
this.isProfileUpdating = true;
|
this.isProfileUpdating = true;
|
||||||
} else if (type === 'password') {
|
|
||||||
this.isPasswordChanging = true;
|
|
||||||
}
|
|
||||||
const hasEmailChanged = this.currentUser.email !== this.email;
|
const hasEmailChanged = this.currentUser.email !== this.email;
|
||||||
try {
|
try {
|
||||||
await this.$store.dispatch('updateProfile', {
|
await this.$store.dispatch('updateProfile', {
|
||||||
name: this.name,
|
name: this.name,
|
||||||
email: this.email,
|
email: this.email,
|
||||||
avatar: this.avatarFile,
|
avatar: this.avatarFile,
|
||||||
password: this.password,
|
|
||||||
displayName: this.displayName,
|
displayName: this.displayName,
|
||||||
password_confirmation: this.passwordConfirmation,
|
|
||||||
});
|
});
|
||||||
this.isProfileUpdating = false;
|
this.isProfileUpdating = false;
|
||||||
this.isPasswordChanging = false;
|
|
||||||
if (hasEmailChanged) {
|
if (hasEmailChanged) {
|
||||||
clearCookiesOnLogout();
|
clearCookiesOnLogout();
|
||||||
this.showAlert(this.$t('PROFILE_SETTINGS.AFTER_EMAIL_CHANGED'));
|
this.errorMessage = 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.UPDATE_SUCCESS');
|
||||||
} catch (error) {
|
} 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.isProfileUpdating = false;
|
||||||
this.isPasswordChanging = false;
|
this.showAlert(this.errorMessage);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
handleImageUpload({ file, url }) {
|
handleImageUpload({ file, url }) {
|
||||||
|
|||||||
@@ -102,12 +102,13 @@ export const actions = {
|
|||||||
},
|
},
|
||||||
|
|
||||||
updateProfile: async ({ commit }, params) => {
|
updateProfile: async ({ commit }, params) => {
|
||||||
|
// eslint-disable-next-line no-useless-catch
|
||||||
try {
|
try {
|
||||||
const response = await authAPI.profileUpdate(params);
|
const response = await authAPI.profileUpdate(params);
|
||||||
setUser(response.data, getHeaderExpiry(response));
|
setUser(response.data, getHeaderExpiry(response));
|
||||||
commit(types.default.SET_CURRENT_USER);
|
commit(types.default.SET_CURRENT_USER);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
// Ignore error
|
throw error;
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
|
|||||||
@@ -39,7 +39,7 @@ RSpec.describe 'Profile API', type: :request do
|
|||||||
end
|
end
|
||||||
|
|
||||||
context 'when it is an authenticated user' do
|
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
|
it 'updates the name & email' do
|
||||||
new_email = Faker::Internet.email
|
new_email = Faker::Internet.email
|
||||||
@@ -56,13 +56,23 @@ RSpec.describe 'Profile API', type: :request do
|
|||||||
expect(agent.email).to eq(new_email)
|
expect(agent.email).to eq(new_email)
|
||||||
end
|
end
|
||||||
|
|
||||||
it 'updates the password' do
|
it 'updates the password when current password is provided' do
|
||||||
put '/api/v1/profile',
|
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,
|
headers: agent.create_new_auth_token,
|
||||||
as: :json
|
as: :json
|
||||||
|
|
||||||
expect(response).to have_http_status(:success)
|
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
|
end
|
||||||
|
|
||||||
it 'updates avatar' do
|
it 'updates avatar' do
|
||||||
|
|||||||
Reference in New Issue
Block a user