API - add current password check when modifying it

This commit is contained in:
Sam 2022-03-13 08:48:37 +01:00
parent fa045915cb
commit 9507a3aba1
9 changed files with 145 additions and 37 deletions

View File

@ -593,25 +593,9 @@ class TestUserAccountUpdate(ApiTestCaseMixin):
headers=dict(Authorization=f'Bearer {auth_token}'), headers=dict(Authorization=f'Bearer {auth_token}'),
) )
self.assert_400(response) self.assert_400(response, error_message='current password is missing')
def test_it_updates_user_profile(self, app: Flask, user_1: User) -> None: def test_it_returns_error_if_current_password_is_missing(
client, auth_token = self.get_test_client_and_auth_token(
app, user_1.email
)
response = client.patch(
'/api/auth/profile/edit/account',
content_type='application/json',
data=json.dumps(dict(password=random_string())),
headers=dict(Authorization=f'Bearer {auth_token}'),
)
assert response.status_code == 200
data = json.loads(response.data.decode())
assert data['status'] == 'success'
assert data['message'] == 'user account updated'
def test_it_returns_error_if_controls_fail(
self, app: Flask, user_1: User self, app: Flask, user_1: User
) -> None: ) -> None:
client, auth_token = self.get_test_client_and_auth_token( client, auth_token = self.get_test_client_and_auth_token(
@ -621,14 +605,99 @@ class TestUserAccountUpdate(ApiTestCaseMixin):
response = client.patch( response = client.patch(
'/api/auth/profile/edit/account', '/api/auth/profile/edit/account',
content_type='application/json', content_type='application/json',
data=json.dumps(dict(password=random_string(length=5))), data=json.dumps(dict(new_password=random_string())),
headers=dict(Authorization=f'Bearer {auth_token}'), headers=dict(Authorization=f'Bearer {auth_token}'),
) )
self.assert_400( self.assert_400(response, error_message='current password is missing')
response, error_message='password: 8 characters required'
def test_it_returns_error_if_current_password_is_invalid(
self, app: Flask, user_1: User
) -> None:
client, auth_token = self.get_test_client_and_auth_token(
app, user_1.email
) )
response = client.patch(
'/api/auth/profile/edit/account',
content_type='application/json',
data=json.dumps(
dict(
password=random_string(),
new_password=random_string(),
)
),
headers=dict(Authorization=f'Bearer {auth_token}'),
)
self.assert_401(response, error_message='invalid credentials')
def test_it_returns_error_if_controls_fail_on_new_password(
self, app: Flask, user_1: User
) -> None:
client, auth_token = self.get_test_client_and_auth_token(
app, user_1.email
)
response = client.patch(
'/api/auth/profile/edit/account',
content_type='application/json',
data=json.dumps(
dict(
password='12345678',
new_password=random_string(length=3),
)
),
headers=dict(Authorization=f'Bearer {auth_token}'),
)
self.assert_400(response, 'password: 8 characters required')
def test_it_updates_auth_user_password(
self, app: Flask, user_1: User
) -> None:
client, auth_token = self.get_test_client_and_auth_token(
app, user_1.email
)
current_hashed_password = user_1.password
response = client.patch(
'/api/auth/profile/edit/account',
content_type='application/json',
data=json.dumps(
dict(
password='12345678',
new_password=random_string(),
)
),
headers=dict(Authorization=f'Bearer {auth_token}'),
)
assert response.status_code == 200
data = json.loads(response.data.decode())
assert data['status'] == 'success'
assert data['message'] == 'user account updated'
assert current_hashed_password != user_1.password
def test_new_password_is_hashed(self, app: Flask, user_1: User) -> None:
client, auth_token = self.get_test_client_and_auth_token(
app, user_1.email
)
new_password = random_string()
response = client.patch(
'/api/auth/profile/edit/account',
content_type='application/json',
data=json.dumps(
dict(
password='12345678',
new_password=new_password,
)
),
headers=dict(Authorization=f'Bearer {auth_token}'),
)
assert response.status_code == 200
assert new_password != user_1.password
class TestUserPreferencesUpdate(ApiTestCaseMixin): class TestUserPreferencesUpdate(ApiTestCaseMixin):
def test_it_updates_user_preferences( def test_it_updates_user_preferences(

View File

@ -626,7 +626,7 @@ def update_user_account(auth_user: User) -> Union[Dict, HttpResponse]:
"username": "sam" "username": "sam"
"weekm": true, "weekm": true,
}, },
"message": "user profile updated", "message": "user account updated",
"status": "success" "status": "success"
} }
@ -646,19 +646,23 @@ def update_user_account(auth_user: User) -> Union[Dict, HttpResponse]:
""" """
data = request.get_json() data = request.get_json()
if not data: if not data or not data.get('password'):
return InvalidPayloadErrorResponse() return InvalidPayloadErrorResponse('current password is missing')
password_data = data.get('password') current_password = data.get('password')
message = check_password(password_data) if not bcrypt.check_password_hash(auth_user.password, current_password):
return UnauthorizedErrorResponse('invalid credentials')
new_password = data.get('new_password')
message = check_password(new_password)
if message != '': if message != '':
return InvalidPayloadErrorResponse(message) return InvalidPayloadErrorResponse(message)
password = bcrypt.generate_password_hash( hashed_password = bcrypt.generate_password_hash(
password_data, current_app.config.get('BCRYPT_LOG_ROUNDS') new_password, current_app.config.get('BCRYPT_LOG_ROUNDS')
).decode() ).decode()
try: try:
auth_user.password = password auth_user.password = hashed_password
db.session.commit() db.session.commit()
return { return {

View File

@ -18,18 +18,29 @@
<input id="email" :value="user.email" disabled /> <input id="email" :value="user.email" disabled />
</label> </label>
<label class="form-items" for="password-field"> <label class="form-items" for="password-field">
{{ $t('user.PASSWORD') }} {{ $t('user.CURRENT_PASSWORD') }}*
<PasswordInput <PasswordInput
id="password-field" id="password-field"
:disabled="loading" :disabled="loading"
:checkStrength="true"
:password="userForm.password" :password="userForm.password"
:isSuccess="false"
:required="true" :required="true"
@updatePassword="updatePassword" @updatePassword="updatePassword"
@passwordError="invalidateForm" @passwordError="invalidateForm"
/> />
</label> </label>
<label class="form-items" for="new-password-field">
{{ $t('user.NEW_PASSWORD') }}*
<PasswordInput
id="new-password-field"
:disabled="loading"
:checkStrength="true"
:password="userForm.new_password"
:isSuccess="false"
:required="true"
@updatePassword="updateNewPassword"
@passwordError="invalidateForm"
/>
</label>
<div class="form-buttons"> <div class="form-buttons">
<button class="confirm" type="submit"> <button class="confirm" type="submit">
{{ $t('buttons.SUBMIT') }} {{ $t('buttons.SUBMIT') }}
@ -74,6 +85,7 @@
const userForm: IUserAccountPayload = reactive({ const userForm: IUserAccountPayload = reactive({
email: '', email: '',
password: '', password: '',
new_password: '',
}) })
const loading = computed( const loading = computed(
() => store.getters[AUTH_USER_STORE.GETTERS.USER_LOADING] () => store.getters[AUTH_USER_STORE.GETTERS.USER_LOADING]
@ -102,9 +114,13 @@
function updatePassword(password: string) { function updatePassword(password: string) {
userForm.password = password userForm.password = password
} }
function updateNewPassword(new_password: string) {
userForm.new_password = new_password
}
function updateProfile() { function updateProfile() {
store.dispatch(AUTH_USER_STORE.ACTIONS.UPDATE_USER_ACCOUNT, { store.dispatch(AUTH_USER_STORE.ACTIONS.UPDATE_USER_ACCOUNT, {
password: userForm.password, password: userForm.password,
new_password: userForm.new_password,
}) })
} }
function updateDisplayModal(value: boolean) { function updateDisplayModal(value: boolean) {
@ -114,15 +130,17 @@
store.dispatch(AUTH_USER_STORE.ACTIONS.DELETE_ACCOUNT, { username }) store.dispatch(AUTH_USER_STORE.ACTIONS.DELETE_ACCOUNT, { username })
} }
onUnmounted(() => onUnmounted(() => {
store.commit(AUTH_USER_STORE.MUTATIONS.UPDATE_IS_SUCCESS, false) store.commit(AUTH_USER_STORE.MUTATIONS.UPDATE_IS_SUCCESS, false)
) store.commit(ROOT_STORE.MUTATIONS.EMPTY_ERROR_MESSAGES)
})
watch( watch(
() => isSuccess.value, () => isSuccess.value,
async (isSuccessValue) => { async (isSuccessValue) => {
if (isSuccessValue) { if (isSuccessValue) {
updatePassword('') updatePassword('')
updateNewPassword('')
formErrors.value = false formErrors.value = false
} }
} }

View File

@ -62,7 +62,7 @@
<script setup lang="ts"> <script setup lang="ts">
import { format } from 'date-fns' import { format } from 'date-fns'
import { ComputedRef, computed, reactive, onMounted } from 'vue' import { ComputedRef, computed, reactive, onMounted, onUnmounted } from 'vue'
import { AUTH_USER_STORE, ROOT_STORE } from '@/store/constants' import { AUTH_USER_STORE, ROOT_STORE } from '@/store/constants'
import { IUserProfile, IUserPayload } from '@/types/user' import { IUserProfile, IUserPayload } from '@/types/user'
@ -115,6 +115,10 @@
function updateProfile() { function updateProfile() {
store.dispatch(AUTH_USER_STORE.ACTIONS.UPDATE_USER_PROFILE, userForm) store.dispatch(AUTH_USER_STORE.ACTIONS.UPDATE_USER_PROFILE, userForm)
} }
onUnmounted(() => {
store.commit(ROOT_STORE.MUTATIONS.EMPTY_ERROR_MESSAGES)
})
</script> </script>
<style lang="scss" scoped> <style lang="scss" scoped>

View File

@ -33,7 +33,7 @@
</template> </template>
<script setup lang="ts"> <script setup lang="ts">
import { ComputedRef, Ref, computed, ref, toRefs } from 'vue' import { ComputedRef, Ref, computed, ref, toRefs, onUnmounted } from 'vue'
import UserPicture from '@/components/User/UserPicture.vue' import UserPicture from '@/components/User/UserPicture.vue'
import { AUTH_USER_STORE, ROOT_STORE } from '@/store/constants' import { AUTH_USER_STORE, ROOT_STORE } from '@/store/constants'
@ -76,6 +76,10 @@
}) })
} }
} }
onUnmounted(() => {
store.commit(ROOT_STORE.MUTATIONS.EMPTY_ERROR_MESSAGES)
})
</script> </script>
<style lang="scss" scoped> <style lang="scss" scoped>

View File

@ -68,7 +68,7 @@
</template> </template>
<script setup lang="ts"> <script setup lang="ts">
import { ComputedRef, computed, reactive, onMounted } from 'vue' import { ComputedRef, computed, reactive, onMounted, onUnmounted } from 'vue'
import TimezoneDropdown from '@/components/User/ProfileEdition/TimezoneDropdown.vue' import TimezoneDropdown from '@/components/User/ProfileEdition/TimezoneDropdown.vue'
import { AUTH_USER_STORE, ROOT_STORE } from '@/store/constants' import { AUTH_USER_STORE, ROOT_STORE } from '@/store/constants'
@ -134,4 +134,8 @@
function updateTZ(value: string) { function updateTZ(value: string) {
userForm.timezone = value userForm.timezone = value
} }
onUnmounted(() => {
store.commit(ROOT_STORE.MUTATIONS.EMPTY_ERROR_MESSAGES)
})
</script> </script>

View File

@ -2,6 +2,7 @@
"ADMIN": "Admin", "ADMIN": "Admin",
"ALREADY_HAVE_ACCOUNT": "Already have an account?", "ALREADY_HAVE_ACCOUNT": "Already have an account?",
"CONFIRM_ACCOUNT_DELETION": "Are you sure you want to delete your account? All data will be deleted, this cannot be undone", "CONFIRM_ACCOUNT_DELETION": "Are you sure you want to delete your account? All data will be deleted, this cannot be undone",
"CURRENT_PASSWORD": "Current password",
"EMAIL": "Email", "EMAIL": "Email",
"EMAIL_INFO": "Enter a valid email address.", "EMAIL_INFO": "Enter a valid email address.",
"ENTER_PASSWORD": "Enter a password", "ENTER_PASSWORD": "Enter a password",
@ -10,6 +11,7 @@
"LANGUAGE": "Language", "LANGUAGE": "Language",
"LOGIN": "Login", "LOGIN": "Login",
"LOGOUT": "Logout", "LOGOUT": "Logout",
"NEW_PASSWORD": "New password",
"PASSWORD": "Password", "PASSWORD": "Password",
"PASSWORD_INFO": "At least 8 characters required.", "PASSWORD_INFO": "At least 8 characters required.",
"PASSWORD_FORGOTTEN": "Forgot password?", "PASSWORD_FORGOTTEN": "Forgot password?",

View File

@ -2,6 +2,7 @@
"ADMIN": "Admin", "ADMIN": "Admin",
"ALREADY_HAVE_ACCOUNT": "Vous avez déjà un compte ?", "ALREADY_HAVE_ACCOUNT": "Vous avez déjà un compte ?",
"CONFIRM_ACCOUNT_DELETION": "Etes-vous sûr de vouloir supprimer votre compte ? Toutes les données seront définitivement effacés.", "CONFIRM_ACCOUNT_DELETION": "Etes-vous sûr de vouloir supprimer votre compte ? Toutes les données seront définitivement effacés.",
"CURRENT_PASSWORD": "Mot de passe actuel",
"EMAIL": "Email", "EMAIL": "Email",
"EMAIL_INFO": "Saisir une adresse email valide.", "EMAIL_INFO": "Saisir une adresse email valide.",
"ENTER_PASSWORD": "Saisir un mot de passe", "ENTER_PASSWORD": "Saisir un mot de passe",
@ -10,6 +11,7 @@
"LANGUAGE": "Langue", "LANGUAGE": "Langue",
"LOGIN": "Se connecter", "LOGIN": "Se connecter",
"LOGOUT": "Se déconnecter", "LOGOUT": "Se déconnecter",
"NEW_PASSWORD": "Nouveau mot de passe",
"PASSWORD": "Mot de passe", "PASSWORD": "Mot de passe",
"PASSWORD_INFO": "8 caractères minimum.", "PASSWORD_INFO": "8 caractères minimum.",
"PASSWORD_FORGOTTEN": "Mot de passe oublié ?", "PASSWORD_FORGOTTEN": "Mot de passe oublié ?",

View File

@ -36,6 +36,7 @@ export interface IUserPayload {
export interface IUserAccountPayload { export interface IUserAccountPayload {
email?: string email?: string
password: string password: string
new_password: string
} }
export interface IAdminUserPayload { export interface IAdminUserPayload {