Client - update password update in user account

This commit is contained in:
Sam 2022-02-26 21:20:11 +01:00
parent a4d7dc24da
commit 7d78bcc302
19 changed files with 276 additions and 73 deletions

View File

@ -1,6 +1,7 @@
<template>
<div class="password-input">
<input
id="password"
:disabled="disabled"
:placeholder="placeholder"
:required="required"
@ -18,13 +19,21 @@
aria-hidden="true"
/>
</div>
<div v-if="checkStrength" class="form-info">
<i class="fa fa-info-circle" aria-hidden="true" />
{{ $t('user.PASSWORD_INFO') }}
</div>
<PasswordStrength v-if="checkStrength" :password="passwordValue" />
</div>
</template>
<script setup lang="ts">
import { Ref, ref, toRefs, watch, withDefaults } from 'vue'
import PasswordStrength from '@/components/Common/PasswordStength.vue'
interface Props {
checkStrength?: boolean
disabled?: boolean
password?: string
placeholder?: string
@ -32,10 +41,13 @@
}
const props = withDefaults(defineProps<Props>(), {
checkStrength: false,
disabled: false,
password: '',
required: false,
})
const { disabled, password, placeholder, required } = toRefs(props)
const { checkStrength, disabled, password, placeholder, required } =
toRefs(props)
const showPassword: Ref<boolean> = ref(false)
const passwordValue: Ref<string> = ref('')

View File

@ -38,14 +38,13 @@
watch,
} from 'vue'
import { ROOT_STORE } from '@/store/constants'
import { AUTH_USER_STORE, ROOT_STORE } from '@/store/constants'
import { useStore } from '@/use/useStore'
import { getPasswordStrength, setZxcvbnOptions } from '@/utils/password'
interface Props {
password: string
}
const props = defineProps<Props>()
const { password } = toRefs(props)
@ -53,6 +52,9 @@
const language: ComputedRef<string> = computed(
() => store.getters[ROOT_STORE.GETTERS.LANGUAGE]
)
const isSuccess: ComputedRef<boolean> = computed(
() => store.getters[AUTH_USER_STORE.GETTERS.IS_SUCCESS]
)
const passwordScore: Ref<number> = ref(0)
const passwordStrength: Ref<string> = ref('')
const passwordSuggestions: Ref<string[]> = ref([])
@ -77,8 +79,12 @@
watch(
() => password.value,
async (newPassword) => {
if (isSuccess.value) {
passwordStrength.value = ''
} else {
calculatePasswordStrength(newPassword)
}
}
)
</script>

View File

@ -0,0 +1,166 @@
<template>
<div id="user-infos-edition">
<Modal
v-if="displayModal"
:title="$t('common.CONFIRMATION')"
:message="$t('user.CONFIRM_ACCOUNT_DELETION')"
@confirmAction="deleteAccount(user.username)"
@cancelAction="updateDisplayModal(false)"
/>
<div class="profile-form form-box">
<ErrorMessage :message="errorMessages" v-if="errorMessages" />
<div class="info-box success-message" v-if="isSuccess">
{{ $t('user.PROFILE.SUCCESSFUL_UPDATE') }}
</div>
<form :class="{ errors: formErrors }" @submit.prevent="updateProfile">
<label class="form-items" for="email">
{{ $t('user.EMAIL') }}
<input id="email" :value="user.email" disabled />
</label>
<label class="form-items" for="password-field">
{{ $t('user.PASSWORD') }}
<PasswordInput
id="password-field"
:disabled="loading"
:checkStrength="true"
:password="userForm.password"
:isSuccess="false"
:required="true"
@updatePassword="updatePassword"
@passwordError="invalidateForm"
/>
</label>
<div class="form-buttons">
<button class="confirm" type="submit">
{{ $t('buttons.SUBMIT') }}
</button>
<button class="cancel" @click.prevent="$router.push('/profile')">
{{ $t('buttons.CANCEL') }}
</button>
<button class="danger" @click.prevent="updateDisplayModal(true)">
{{ $t('buttons.DELETE_MY_ACCOUNT') }}
</button>
</div>
</form>
</div>
</div>
</template>
<script setup lang="ts">
import {
ComputedRef,
Ref,
computed,
reactive,
ref,
toRefs,
onMounted,
watch,
onUnmounted,
} from 'vue'
import PasswordInput from '@/components/Common/PasswordInput.vue'
import { AUTH_USER_STORE, ROOT_STORE } from '@/store/constants'
import { IUserProfile, IUserAccountPayload } from '@/types/user'
import { useStore } from '@/use/useStore'
interface Props {
user: IUserProfile
}
const props = defineProps<Props>()
const { user } = toRefs(props)
const store = useStore()
const userForm: IUserAccountPayload = reactive({
email: '',
password: '',
})
const loading = computed(
() => store.getters[AUTH_USER_STORE.GETTERS.USER_LOADING]
)
const isSuccess: ComputedRef<boolean> = computed(
() => store.getters[AUTH_USER_STORE.GETTERS.IS_SUCCESS]
)
const errorMessages: ComputedRef<string | string[] | null> = computed(
() => store.getters[ROOT_STORE.GETTERS.ERROR_MESSAGES]
)
const formErrors = ref(false)
const displayModal: Ref<boolean> = ref(false)
onMounted(() => {
if (props.user) {
updateUserForm(props.user)
}
})
function invalidateForm() {
formErrors.value = true
}
function updateUserForm(user: IUserProfile) {
userForm.email = user.email
}
function updatePassword(password: string) {
userForm.password = password
}
function updateProfile() {
store.dispatch(AUTH_USER_STORE.ACTIONS.UPDATE_USER_ACCOUNT, {
password: userForm.password,
})
}
function updateDisplayModal(value: boolean) {
displayModal.value = value
}
function deleteAccount(username: string) {
store.dispatch(AUTH_USER_STORE.ACTIONS.DELETE_ACCOUNT, { username })
}
onUnmounted(() =>
store.commit(AUTH_USER_STORE.MUTATIONS.UPDATE_IS_SUCCESS, false)
)
watch(
() => isSuccess.value,
async (isSuccessValue) => {
if (isSuccessValue) {
updatePassword('')
formErrors.value = false
}
}
)
</script>
<style lang="scss" scoped>
@import '~@/scss/vars.scss';
.form-items {
.password-input {
::v-deep(.show-password) {
font-weight: normal;
font-size: 0.8em;
margin-top: -4px;
padding-left: 0;
}
::v-deep(.form-info) {
font-weight: normal;
padding-left: $default-padding;
}
::v-deep(.password-strength-details) {
font-weight: normal;
margin-top: 0;
}
}
}
.form-buttons {
flex-direction: row;
@media screen and (max-width: $x-small-limit) {
flex-direction: column;
}
}
.success-message {
margin: $default-margin * 2 0;
background-color: var(--success-background-color);
color: var(--success-color);
}
</style>

View File

@ -1,32 +1,12 @@
<template>
<div id="user-infos-edition">
<Modal
v-if="displayModal"
:title="$t('common.CONFIRMATION')"
:message="$t('user.CONFIRM_ACCOUNT_DELETION')"
@confirmAction="deleteAccount(user.username)"
@cancelAction="updateDisplayModal(false)"
/>
<div class="profile-form form-box">
<ErrorMessage :message="errorMessages" v-if="errorMessages" />
<form @submit.prevent="updateProfile">
<label class="form-items" for="email">
{{ $t('user.EMAIL') }}
<input id="email" :value="user.email" disabled />
</label>
<label class="form-items" for="registrationDate">
{{ $t('user.PROFILE.REGISTRATION_DATE') }}
<input id="registrationDate" :value="registrationDate" disabled />
</label>
<label class="form-items" for="password">
{{ $t('user.PASSWORD') }}
<PasswordInput
id="password"
:disabled="loading"
@updatePassword="updatePassword"
/>
</label>
<hr />
<label class="form-items" for="first_name">
{{ $t('user.PROFILE.FIRST_NAME') }}
<input
@ -74,9 +54,6 @@
<button class="cancel" @click.prevent="$router.push('/profile')">
{{ $t('buttons.CANCEL') }}
</button>
<button class="danger" @click.prevent="updateDisplayModal(true)">
{{ $t('buttons.DELETE_MY_ACCOUNT') }}
</button>
</div>
</form>
</div>
@ -85,17 +62,8 @@
<script setup lang="ts">
import { format } from 'date-fns'
import {
ComputedRef,
Ref,
computed,
reactive,
ref,
toRefs,
onMounted,
} from 'vue'
import { ComputedRef, computed, reactive, onMounted } from 'vue'
import PasswordInput from '@/components/Common/PasswordInput.vue'
import { AUTH_USER_STORE, ROOT_STORE } from '@/store/constants'
import { IUserProfile, IUserPayload } from '@/types/user'
import { useStore } from '@/use/useStore'
@ -107,9 +75,7 @@
const store = useStore()
const { user } = toRefs(props)
const userForm: IUserPayload = reactive({
password: '',
first_name: '',
last_name: '',
birth_date: '',
@ -127,7 +93,6 @@
const errorMessages: ComputedRef<string | string[] | null> = computed(
() => store.getters[ROOT_STORE.GETTERS.ERROR_MESSAGES]
)
let displayModal: Ref<boolean> = ref(false)
onMounted(() => {
if (props.user) {
@ -147,18 +112,9 @@
function updateBio(value: string) {
userForm.bio = value
}
function updatePassword(password: string) {
userForm.password = password
}
function updateProfile() {
store.dispatch(AUTH_USER_STORE.ACTIONS.UPDATE_USER_PROFILE, userForm)
}
function updateDisplayModal(value: boolean) {
displayModal.value = value
}
function deleteAccount(username: string) {
store.dispatch(AUTH_USER_STORE.ACTIONS.DELETE_ACCOUNT, { username })
}
</script>
<style lang="scss" scoped>

View File

@ -34,7 +34,7 @@
const store = useStore()
const { user, tab } = toRefs(props)
const tabs = ['PROFILE', 'PICTURE', 'PREFERENCES', 'SPORTS']
const tabs = ['PROFILE', 'ACCOUNT', 'PICTURE', 'PREFERENCES', 'SPORTS']
const loading = computed(
() => store.getters[AUTH_USER_STORE.GETTERS.USER_LOADING]
)

View File

@ -64,21 +64,10 @@
: $t('user.PASSWORD')
"
:password="formData.password"
:checkStrength="['reset', 'register'].includes(action)"
@updatePassword="updatePassword"
@passwordError="invalidateForm"
/>
<div
v-if="['reset', 'register'].includes(action)"
class="form-info"
>
<i class="fa fa-info-circle" aria-hidden="true" />
{{ $t('user.PASSWORD_INFO') }}
</div>
<PasswordStrength
v-if="['reset', 'register'].includes(action)"
:password="formData.password"
/>
</div>
<button type="submit" :disabled="registration_disabled">
{{ $t(buttonText) }}
@ -118,7 +107,6 @@
import { useRoute } from 'vue-router'
import PasswordInput from '@/components/Common/PasswordInput.vue'
import PasswordStrength from '@/components/Common/PasswordStength.vue'
import { AUTH_USER_STORE, ROOT_STORE } from '@/store/constants'
import { TAppConfig } from '@/types/application'
import { ILoginRegisterFormData } from '@/types/user'
@ -234,13 +222,6 @@
font-style: italic;
padding: 0 $default-padding;
}
.form-info {
color: var(--alert-color);
font-size: 0.8em;
margin-top: -0.2 * $default-margin;
padding: 0 $default-padding * 1.5;
}
button {
margin: $default-margin;
border: solid 1px var(--app-color);

View File

@ -35,8 +35,9 @@
function getPath(tab: string) {
switch (tab) {
case 'ACCOUNT':
case 'PICTURE':
return '/profile/edit/picture'
return `/profile/edit/${tab.toLocaleLowerCase()}`
case 'PREFERENCES':
case 'SPORTS':
return `/profile${
@ -52,7 +53,10 @@
<style lang="scss">
@import '~@/scss/vars.scss';
.profile-tabs {
margin: $default-margin 0 $default-margin;
.profile-tabs-checkboxes {
display: flex;
justify-content: center;
flex-wrap: wrap;
gap: $default-margin * 0.5;
}
</style>

View File

@ -40,6 +40,7 @@
},
"PASSWORD_UPDATED": "Your password have been updated. Click {0} to log in.",
"PROFILE": {
"ACCOUNT_EDITION": "Account edition",
"BACK_TO_PROFILE": "Back to profile",
"BIO": "Bio",
"BIRTH_DATE": "Birth date",
@ -62,6 +63,7 @@
"SPORTS_EDITION": "Sports preferences edition",
"SUNDAY": "Sunday",
"TABS": {
"ACCOUNT": "account",
"PICTURE": "picture",
"PREFERENCES": "preferences",
"PROFILE": "profile",
@ -75,6 +77,7 @@
"LABEL": "label",
"STOPPED_SPEED_THRESHOLD": "stopped speed threshold"
},
"SUCCESSFUL_UPDATE": "Your account has been updated successfully",
"UNITS": {
"LABEL": "Units for distance",
"IMPERIAL": "Imperial system (ft, mi)",

View File

@ -39,6 +39,7 @@
},
"PASSWORD_UPDATED": "Votre mot de passe a été mis à jour. Cliquez {0} pour vous connecter.",
"PROFILE": {
"ACCOUNT_EDITION": "Mise à jour du compte",
"BACK_TO_PROFILE": "Revenir au profil",
"BIO": "Bio",
"BIRTH_DATE": "Date de naissance",
@ -61,6 +62,7 @@
"SPORTS_EDITION": "Mise à jour des préférences des sports",
"SUNDAY": "Dimanche",
"TABS": {
"ACCOUNT": "compte",
"PICTURE": "image",
"PREFERENCES": "préférences",
"PROFILE": "profil",
@ -79,6 +81,7 @@
"LABEL": "label",
"STOPPED_SPEED_THRESHOLD": "seuil de vitesse arrêtée"
},
"SUCCESSFUL_UPDATE": "Votre compte a été modifié avec succès",
"TIMEZONE": "Fuseau horaire"
},
"REGISTER": "S'inscrire",

View File

@ -8,6 +8,7 @@ import Profile from '@/components/User/ProfileDisplay/index.vue'
import UserInfos from '@/components/User/ProfileDisplay/UserInfos.vue'
import UserPreferences from '@/components/User/ProfileDisplay/UserPreferences.vue'
import ProfileEdition from '@/components/User/ProfileEdition/index.vue'
import UserAccountEdition from '@/components/User/ProfileEdition/UserAccountEdition.vue'
import UserInfosEdition from '@/components/User/ProfileEdition/UserInfosEdition.vue'
import UserPictureEdition from '@/components/User/ProfileEdition/UserPictureEdition.vue'
import UserPreferencesEdition from '@/components/User/ProfileEdition/UserPreferencesEdition.vue'
@ -123,6 +124,11 @@ const routes: Array<RouteRecordRaw> = [
name: 'UserInfosEdition',
component: UserInfosEdition,
},
{
path: 'account',
name: 'UserAccountEdition',
component: UserAccountEdition,
},
{
path: 'picture',
name: 'UserPictureEdition',

View File

@ -142,6 +142,14 @@ button {
}
}
.form-info {
color: var(--alert-color);
font-size: 0.8em;
margin-top: -0.2 * $default-margin;
padding: 0 $default-padding * 1.5;
}
.upper {
text-transform: uppercase;
}

View File

@ -50,6 +50,8 @@
--info-color: var(--app-color);
--error-background-color: #ffd2d2;
--error-color: #db1924;
--success-background-color: #d9ecd9;
--success-color: #306430;
--disabled-background-color: #e0e0e0;
--disabled-color: #a3a3a3;

View File

@ -20,6 +20,7 @@ import { IRootState } from '@/store/modules/root/types'
import { deleteUserAccount } from '@/store/modules/users/actions'
import {
ILoginOrRegisterData,
IUserAccountPayload,
IUserDeletionPayload,
IUserPasswordPayload,
IUserPasswordResetPayload,
@ -145,6 +146,31 @@ export const actions: ActionTree<IAuthUserState, IRootState> &
context.commit(AUTH_USER_STORE.MUTATIONS.UPDATE_USER_LOADING, false)
)
},
[AUTH_USER_STORE.ACTIONS.UPDATE_USER_ACCOUNT](
context: ActionContext<IAuthUserState, IRootState>,
payload: IUserAccountPayload
): void {
context.commit(ROOT_STORE.MUTATIONS.EMPTY_ERROR_MESSAGES)
context.commit(AUTH_USER_STORE.MUTATIONS.UPDATE_USER_LOADING, true)
context.commit(AUTH_USER_STORE.MUTATIONS.UPDATE_IS_SUCCESS, false)
authApi
.patch('auth/profile/edit/account', payload)
.then((res) => {
if (res.data.status === 'success') {
context.commit(
AUTH_USER_STORE.MUTATIONS.UPDATE_AUTH_USER_PROFILE,
res.data.data
)
context.commit(AUTH_USER_STORE.MUTATIONS.UPDATE_IS_SUCCESS, true)
} else {
handleError(context, null)
}
})
.catch((error) => handleError(context, error))
.finally(() =>
context.commit(AUTH_USER_STORE.MUTATIONS.UPDATE_USER_LOADING, false)
)
},
[AUTH_USER_STORE.ACTIONS.UPDATE_USER_PREFERENCES](
context: ActionContext<IAuthUserState, IRootState>,
payload: IUserPreferencesPayload

View File

@ -8,6 +8,7 @@ export enum AuthUserActions {
SEND_PASSWORD_RESET_REQUEST = 'SEND_PASSWORD_RESET_REQUEST',
RESET_USER_PASSWORD = 'RESET_USER_PASSWORD',
RESET_USER_SPORT_PREFERENCES = 'RESET_USER_SPORT_PREFERENCES',
UPDATE_USER_ACCOUNT = 'UPDATE_USER_ACCOUNT',
UPDATE_USER_PICTURE = 'UPDATE_USER_PICTURE',
UPDATE_USER_PROFILE = 'UPDATE_USER_PROFILE',
UPDATE_USER_PREFERENCES = 'UPDATE_USER_PREFERENCES',
@ -19,6 +20,7 @@ export enum AuthUserGetters {
AUTH_USER_PROFILE = 'AUTH_USER_PROFILE',
IS_ADMIN = 'IS_ADMIN',
IS_AUTHENTICATED = 'IS_AUTHENTICATED',
IS_SUCCESS = 'IS_SUCCESS',
USER_LOADING = 'USER_LOADING',
}
@ -26,5 +28,6 @@ export enum AuthUserMutations {
CLEAR_AUTH_USER_TOKEN = 'CLEAR_AUTH_USER_TOKEN',
UPDATE_AUTH_TOKEN = 'UPDATE_AUTH_TOKEN',
UPDATE_AUTH_USER_PROFILE = 'UPDATE_AUTH_USER_PROFILE',
UPDATE_IS_SUCCESS = 'UPDATE_USER_IS_SUCCESS',
UPDATE_USER_LOADING = 'UPDATE_USER_LOADING',
}

View File

@ -21,6 +21,9 @@ export const getters: GetterTree<IAuthUserState, IRootState> &
[AUTH_USER_STORE.GETTERS.IS_ADMIN]: (state: IAuthUserState) => {
return state.authUserProfile && state.authUserProfile.admin
},
[AUTH_USER_STORE.GETTERS.IS_SUCCESS]: (state: IAuthUserState) => {
return state.isSuccess
},
[AUTH_USER_STORE.GETTERS.USER_LOADING]: (state: IAuthUserState) => {
return state.loading
},

View File

@ -24,6 +24,12 @@ export const mutations: MutationTree<IAuthUserState> & TAuthUserMutations = {
) {
state.authUserProfile = authUserProfile
},
[AUTH_USER_STORE.MUTATIONS.UPDATE_IS_SUCCESS](
state: IAuthUserState,
isSuccess: boolean
) {
state.isSuccess = isSuccess
},
[AUTH_USER_STORE.MUTATIONS.UPDATE_USER_LOADING](
state: IAuthUserState,
loading: boolean

View File

@ -4,5 +4,6 @@ import { IUserProfile } from '@/types/user'
export const authUserState: IAuthUserState = {
authToken: null,
authUserProfile: <IUserProfile>{},
isSuccess: false,
loading: false,
}

View File

@ -17,11 +17,13 @@ import {
IUserPicturePayload,
IUserPreferencesPayload,
IUserSportPreferencesPayload,
IUserAccountPayload,
} from '@/types/user'
export interface IAuthUserState {
authToken: string | null
authUserProfile: IUserProfile
isSuccess: boolean
loading: boolean
}
@ -48,6 +50,11 @@ export interface IAuthUserActions {
payload: IUserPayload
): void
[AUTH_USER_STORE.ACTIONS.UPDATE_USER_ACCOUNT](
context: ActionContext<IAuthUserState, IRootState>,
payload: IUserAccountPayload
): void
[AUTH_USER_STORE.ACTIONS.UPDATE_USER_PREFERENCES](
context: ActionContext<IAuthUserState, IRootState>,
payload: IUserPreferencesPayload
@ -99,6 +106,8 @@ export interface IAuthUserGetters {
[AUTH_USER_STORE.GETTERS.IS_AUTHENTICATED](state: IAuthUserState): boolean
[AUTH_USER_STORE.GETTERS.IS_SUCCESS](state: IAuthUserState): boolean
[AUTH_USER_STORE.GETTERS.USER_LOADING](state: IAuthUserState): boolean
}
@ -112,6 +121,10 @@ export type TAuthUserMutations<S = IAuthUserState> = {
state: S,
authUserProfile: IUserProfile
): void
[AUTH_USER_STORE.MUTATIONS.UPDATE_IS_SUCCESS](
state: S,
isSuccess: boolean
): void
[AUTH_USER_STORE.MUTATIONS.UPDATE_USER_LOADING](
state: S,
loading: boolean

View File

@ -31,6 +31,10 @@ export interface IUserPayload {
first_name: string
last_name: string
location: string
}
export interface IUserAccountPayload {
email?: string
password: string
}