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:
Muhsin Keloth 2021-06-02 17:52:24 +05:30 committed by GitHub
parent 2c42e70637
commit b0b4d9d6f5
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
6 changed files with 201 additions and 84 deletions

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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