Skip to content

Commit

Permalink
Feature/bus 928 password reset (#56)
Browse files Browse the repository at this point in the history
* [BUS-928] reset password flow

* [BUS-928] fix problem resulting from merge conflict

* [BUS-928] code review changes
  • Loading branch information
MorennMcFly authored Oct 1, 2024
1 parent ebf4f9c commit b10aa74
Show file tree
Hide file tree
Showing 14 changed files with 289 additions and 11 deletions.
24 changes: 21 additions & 3 deletions components/Account/AccountLogin.vue
Original file line number Diff line number Diff line change
Expand Up @@ -12,14 +12,18 @@ const props = withDefaults(
redirectAfterSuccess?: boolean;
redirectTarget?: string;
showCreateLink?: boolean;
showRecoverLink?: boolean;
}>(),
{
redirectAfterSuccess: false,
redirectTarget: '/account',
showCreateLink: true,
showRecoverLink: true,
},
);
defineEmits(['closeModal']);
const customerStore = useCustomerStore();
const { togglePasswordVisibility } = useFormkitHelper();
const { resolveApiErrors } = useApiErrorsResolver();
Expand Down Expand Up @@ -91,11 +95,25 @@ const handleLogin = async (fields: FormkitLoginFields) => {
suffix-icon="lock"
@suffix-icon-click="togglePasswordVisibility"
/>
</FormKit>

<div class="mt-2 flex justify-between">
<NuxtLink
v-if="showCreateLink"
:to="{ name: 'account-register' }"
>{{ $t('account.login.createAccount') }}</NuxtLink
to="/account/register"
class="hover:text-brand-primary"
>
</FormKit>
{{ $t('account.login.createAccountLink') }}
</NuxtLink>

<NuxtLink
v-if="showRecoverLink"
to="/account/recover"
class="hover:text-brand-primary"
:class="{'text-right': showCreateLink}"
@click="$emit('closeModal')"
>
{{ $t('account.login.recoverPasswordLink') }}
</NuxtLink>
</div>
</template>
9 changes: 8 additions & 1 deletion components/Account/AccountLoginRegisterTabs.vue
Original file line number Diff line number Diff line change
@@ -1,3 +1,7 @@
<script setup lang="ts">
defineEmits(['closeModal']);
</script>

<template>
<TabsRoot
class="flex w-full flex-col"
Expand All @@ -24,7 +28,10 @@
class="grow rounded-b-md bg-white py-5 outline-none"
value="tab-login"
>
<AccountLogin :show-create-link="false" />
<AccountLogin
:show-create-link="false"
@close-modal="$emit('closeModal')"
/>
</TabsContent>
<TabsContent
class="grow rounded-b-md bg-white py-5 outline-none"
Expand Down
71 changes: 71 additions & 0 deletions components/Account/AccountRecoverPassword.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
<script setup lang="ts">
import type { FormkitFields } from '~/types/formkit';
const customerStore = useCustomerStore();
const { query } = useRoute();
const { t } = useI18n();
const { pushSuccess, pushError } = useNotifications();
const hashIsValid = ref(true);
onMounted(async () => {
try {
const response = await customerStore.isRecoveryHashValid({ hash: query.hash });
hashIsValid.value = !response.isExpired;
} catch (error) {
hashIsValid.value = false;
}
});
const handlePasswordChange = async (fields: FormkitFields) => {
try {
await customerStore.recoverPassword({
hash: query.hash,
...fields,
});
pushSuccess(t('account.recoverPassword.recover.successMessage'));
navigateTo('/account/login');
} catch (error) {
pushError(t('account.recoverPassword.recover.errorMessage'));
}
};
</script>

<template>
<template v-if="!hashIsValid">
<p class="text-center text-status-danger">{{ $t('account.recoverPassword.recover.linkNotValid') }}</p>
<NuxtLink
to="/account/recover"
class="m-auto mt-4 flex w-1/2 items-center justify-center rounded bg-brand-primary px-4 py-2 text-white hover:bg-brand-primary-dark"
>
{{ $t('account.recoverPassword.recover.requestNewLink') }}
</NuxtLink>
</template>

<FormKit
v-else
type="form"
:submit-label="$t('account.recoverPassword.recover.submitLabel')"
:classes="{
form: 'w-full flex flex-wrap flex-col gap-4',
actions: 'w-full',
}"
@submit="handlePasswordChange"
>
<h5>{{ $t('account.recoverPassword.recover.heading') }}</h5>
<FormKit
type="password"
:label="$t('account.recoverPassword.recover.passwordLabel')"
name="newPassword"
:placeholder="$t('account.login.email.placeholder')"
validation="required|length:8"
/>

<FormKit
type="password"
:label="$t('account.recoverPassword.recover.confirmLabel')"
name="newPasswordConfirm"
:placeholder="$t('account.login.email.placeholder')"
validation="required|confirm:newPassword"
/>
</FormKit>
</template>
62 changes: 62 additions & 0 deletions components/Account/AccountResetPassword.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
<script setup lang="ts">
import type { FormkitFields } from '~/types/formkit';
const customerStore = useCustomerStore();
const { getStorefrontUrl } = useInternationalization();
const formSent = ref(false);
const handleReset = async (fields: FormkitFields) => {
try {
await customerStore.resetPassword({
...fields,
storefrontUrl: getEnvironmentStorefrontUrl(),
});
} catch (error) {
// we won't do anything here because we don't want the user to know if the email is registered
} finally {
formSent.value = true;
}
};
// helper method to get a correct url for testing in dev mode
// TODO: maybe change for prod env?
const getEnvironmentStorefrontUrl = (): string => {
const storefrontUrl = getStorefrontUrl();
return import.meta.dev ? storefrontUrl + ':3000' : storefrontUrl;
};
</script>

<template>
<h3 class="mb-4">{{ $t('account.recoverPassword.request.heading') }}</h3>
<FormKit
v-if="!formSent"
type="form"
:submit-label="$t('account.recoverPassword.request.submitLabel')"
:classes="{
form: 'w-full flex flex-wrap flex-col gap-4',
actions: 'w-full',
}"
@submit="handleReset"
>
<p class="text-sm">{{ $t('account.recoverPassword.request.subHeading') }}</p>
<FormKit
type="email"
:label="$t('account.login.email.label')"
name="email"
:placeholder="$t('account.login.email.placeholder')"
validation="required|email"
/>
</FormKit>

<template v-if="formSent">
<p>
{{ $t('account.recoverPassword.request.successMessage') }}
</p>
<NuxtLink
to="/account/login"
class="mt-4 flex w-full justify-center rounded bg-brand-primary px-4 py-2 text-white hover:bg-brand-primary-dark"
>
{{ $t('account.recoverPassword.request.loginButtonLabel') }}
</NuxtLink>
</template>
</template>
8 changes: 6 additions & 2 deletions components/Layout/Header/LayoutHeaderAccount.vue
Original file line number Diff line number Diff line change
Expand Up @@ -2,11 +2,16 @@
const customerStore = useCustomerStore();
const { signedIn } = storeToRefs(customerStore);
const modalController = useModal();
const closeModal = () => {
modalController.close();
};
</script>

<template>
<LazySharedModal
v-if="!signedIn"
:controller="modalController"
:with-close-button="true"
>
<template #trigger>
Expand All @@ -17,15 +22,14 @@ const modalController = useModal();
</template>
<template #title>{{ $t('account.loginModal.heading') }}</template>
<template #content>
<AccountLoginRegisterTabs />
<AccountLoginRegisterTabs @close-modal="closeModal" />
</template>
</LazySharedModal>
<LazySharedPopover v-else>
<template #trigger>
<FormKitIcon
class="block h-6 w-6"
icon="user"
@click="!signedIn ? modalController.open() : null"
/>
</template>
<template #content>
Expand Down
29 changes: 29 additions & 0 deletions composables/usePasswordRecovery.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
import type { operations } from '@shopware/api-client/api-types';

export function usePasswordRecovery() {
const { apiClient } = useShopwareContext();

async function recoverPassword(
recoverPasswordData: operations['recoveryPassword post /account/recovery-password-confirm']['body'],
) {
const response = await apiClient.invoke('recoveryPassword post /account/recovery-password-confirm', {
body: recoverPasswordData,
});
return response.data;
}

async function isRecoveryHashValid(recoverHash: { hash: string }) {
const response = await apiClient.invoke(
'getCustomerRecoveryIsExpired post /account/customer-recovery-is-expired',
{
body: recoverHash,
},
);
return response.data;
}

return {
isRecoveryHashValid,
recoverPassword,
};
}
17 changes: 16 additions & 1 deletion composables/useStaticBreadcrumbs.ts
Original file line number Diff line number Diff line change
Expand Up @@ -37,11 +37,26 @@ export function useStaticBreadcrumbs() {
name: t('composable.breadcrumbs.loginName'),
path: '/account/login',
});
} else if (type === 'register') {

return breadcrumbs;
}

if (type === 'register') {
breadcrumbs.push({
name: t('composable.breadcrumbs.registerName'),
path: '/account/register',
});

return breadcrumbs;
}

if (type === 'recover') {
breadcrumbs.push({
name: t('composable.breadcrumbs.recoverName'),
path: '/account/recover',
});

return breadcrumbs;
}

return breadcrumbs;
Expand Down
22 changes: 21 additions & 1 deletion i18n/de-DE/account.json
Original file line number Diff line number Diff line change
Expand Up @@ -53,7 +53,8 @@
"password": {
"label": "Passwort"
},
"createAccountLink": "Erstelle hier deinen Account"
"createAccountLink": "Erstelle hier deinen Account",
"recoverPasswordLink": "Passwort vergessen?"
},
"register": {
"submitLabel": "Registrieren",
Expand Down Expand Up @@ -105,6 +106,25 @@
"logout": {
"buttonLabel": "Logout"
},
"recoverPassword": {
"request": {
"heading": "Passwort wiederherstellen",
"subHeading": "Wir schicken dir eine Email zu. Klicke auf den Link in der Email, um ein neues Passwort zu vergeben.",
"submitLabel": "Email anfordern",
"successMessage": "Falls deine Emailadresse bei uns registriert ist, haben wir dir eine Email geschickt. Bitte schau in dein Postfach.",
"loginButtonLabel": "Zum Login"
},
"recover": {
"heading": "Lege ein neues Passwort fest",
"submitLabel": "Passwort ändern",
"errorMessage": "Etwas ist beim Ändern deines Passworts schiefgegangen.",
"successMessage": "Dein Passwort wurde erfolgreich geändert.",
"passwordLabel": "Neues Passwort",
"confirmLabel": "Neues Passwort bestätigen",
"linkNotValid": "Dieser Link scheint nicht gültig zu sein. Bitte fordere einen neuen an.",
"requestNewLink": "Neuen Link anfordern"
}
},
"loginModal": {
"heading": "Login",
"tabs": {
Expand Down
3 changes: 2 additions & 1 deletion i18n/de-DE/composable.json
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,8 @@
"orderName": "Bestellung",
"accountName": "Konto",
"loginName": "Login",
"registerName": "Registrieren"
"registerName": "Registrieren",
"recoverName": "Passwort wiederherstellen"
}
}
}
21 changes: 20 additions & 1 deletion i18n/en-GB/account.json
Original file line number Diff line number Diff line change
Expand Up @@ -53,7 +53,8 @@
"password": {
"label": "Password"
},
"createAccountLink": "Create your account here"
"createAccountLink": "Create your account here",
"recoverPasswordLink": "Forgot password?"
},
"register": {
"submitLabel": "Register",
Expand Down Expand Up @@ -105,6 +106,24 @@
"logout": {
"buttonLabel": "Logout"
},
"recoverPassword": {
"request": {
"heading": "Password Recovery",
"subHeading": "We will send you an email. Click the link in that email in order to change your password.",
"submitLabel": "Request Email",
"successMessage": "If your email address is registered with us, we sent you an email. Please check your inbox.",
"loginButtonLabel": "Login"
},
"recover": {
"heading": "Choose a new password",
"submitLabel": "Change password",
"errorMessage": "Something went wrong changing your password.",
"passwordLabel": "New password",
"confirmLabel": "Confirm new password",
"linkNotValid": "This link doesn't seem to be valid. Please request another.",
"requestNewLink": "Request a new recovery link"
}
},
"loginModal": {
"heading": "Login",
"tabs": {
Expand Down
3 changes: 2 additions & 1 deletion i18n/en-GB/composable.json
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,8 @@
"orderName": "Order",
"accountName": "Account",
"loginName": "Login",
"registerName": "Register"
"registerName": "Register",
"recoverName": "Recover password"
}
}
}
Loading

0 comments on commit b10aa74

Please sign in to comment.