API - add current password check when modifying it
This commit is contained in:
parent
fa045915cb
commit
9507a3aba1
@ -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(
|
||||||
|
@ -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 {
|
||||||
|
@ -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
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -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>
|
||||||
|
@ -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>
|
||||||
|
@ -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>
|
||||||
|
@ -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?",
|
||||||
|
@ -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é ?",
|
||||||
|
@ -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 {
|
||||||
|
Loading…
Reference in New Issue
Block a user