account-page-password-reset #14

Merged
tiradoe merged 3 commits from account-page-password-reset into main 2026-04-20 20:10:23 +00:00
9 changed files with 128 additions and 13 deletions

View file

@ -9,5 +9,6 @@
--color-button-warning: #f59e0b; --color-button-warning: #f59e0b;
--color-button-danger: #fb3b3b; --color-button-danger: #fb3b3b;
--color-action-button-text: #fff; --color-action-button-text: #fff;
--color-success-text: #4caf50;
--color-error-text: #fb3b3b; --color-error-text: #fb3b3b;
} }

View file

@ -4,14 +4,14 @@ const props = defineProps<{
email: string email: string
}>(); }>();
const {resetPassword} = useAuth(); const {resetPasswordWithToken} = useAuth();
const password = ref(""); const password = ref("");
const passwordConfirmation = ref(""); const passwordConfirmation = ref("");
const tokenExpired = ref(false); const tokenExpired = ref(false);
const handlePasswordReset = () => { const handlePasswordReset = () => {
try { try {
resetPassword(password.value, passwordConfirmation.value, props.token, props.email); resetPasswordWithToken(password.value, passwordConfirmation.value, props.token, props.email);
} catch (error: unknown) { } catch (error: unknown) {
if (error instanceof Error && error.message === "TOKEN_EXPIRED") if (error instanceof Error && error.message === "TOKEN_EXPIRED")
tokenExpired.value = true; tokenExpired.value = true;

View file

@ -1,18 +1,49 @@
<script lang="ts" setup> <script lang="ts" setup>
import ButtonAction from "~/components/common/button-action.vue";
const {resetPassword} = useAuth();
const currentPassword = ref("");
const newPassword = ref("");
const confirmNewPassword = ref("");
const errors = ref<string[]>([]);
const successMessage = ref("");
const handlePasswordReset = async () => {
errors.value = []
try {
await resetPassword(newPassword.value, confirmNewPassword.value, currentPassword.value);
successMessage.value = "Password reset successful!";
} catch (error) {
const fieldErrors = Object.values((error as any)?.errors ?? {}).flat() as string[]
errors.value = fieldErrors.length ? fieldErrors : [error?.message ?? "An error occurred. Please try again."]
successMessage.value = "";
}
}
</script> </script>
<template> <template>
<form class="password-form"> <form class="password-form" @submit.prevent>
<div class="form-group"> <div class="form-group">
<label for="current-password">Current Password</label> <label for="new-password">Current Password</label>
<input id="current-password" type="password"/> <input id="new-password" v-model="currentPassword" type="password"/>
</div>
<div class="form-group">
<label for="current-password">New Password</label>
<input id="current-password" v-model="newPassword" type="password"/>
</div>
<div class="form-group">
<label for="current-password">New Password (again)</label>
<input id="current-password" v-model="confirmNewPassword" type="password"/>
</div> </div>
<div class="form-group">
<label for="new-password">New Password</label> <ButtonAction button-text="Reset Password" @action="handlePasswordReset"/>
<input id="new-password" type="password"/> <ul v-if="errors.length" class="error-message">
</div> <li v-for="msg in errors" :key="msg">{{ msg }}</li>
</ul>
<p v-if="successMessage" class="success-message">{{ successMessage }}</p>
</form> </form>
</template> </template>
@ -22,4 +53,12 @@
flex-direction: column; flex-direction: column;
gap: 1rem; gap: 1rem;
} }
.error-message {
color: var(--color-error-text);
}
.success-message {
color: var(--color-success-text);
}
</style> </style>

View file

@ -26,7 +26,7 @@ onUnmounted(() => document.removeEventListener('click', onClickOutside))
<ul v-if="dropdownOpen" class="dropdown"> <ul v-if="dropdownOpen" class="dropdown">
<li> <li>
<NuxtLink to="/account" @click="dropdownOpen = false">Account</NuxtLink> <NuxtLink to="/account-settings" @click="dropdownOpen = false">Account</NuxtLink>
</li> </li>
<li @click="logout">Log Out</li> <li @click="logout">Log Out</li>
</ul> </ul>

View file

@ -37,12 +37,32 @@ export const useAuth = () => {
await navigateTo('/auth/login') await navigateTo('/auth/login')
} }
const resetPassword = async (password: string, passwordConfirmation: string, token: string, email: string) => { const resetPassword = async (password: string, passwordConfirmation: string, currentPassword: string) => {
await $fetch('/sanctum/csrf-cookie', { await $fetch('/sanctum/csrf-cookie', {
baseURL: config.public.apiBase, baseURL: config.public.apiBase,
credentials: 'include', credentials: 'include',
}) })
await $api('/api/reset-password', { await $api('/api/reset-password', {
method: 'POST',
body: {
password,
password_confirmation: passwordConfirmation,
current_password: currentPassword
},
onResponseError: ({response}) => {
const err = new Error(response._data?.message ?? 'Failed to reset password');
(err as any).errors = response._data?.errors ?? {}
throw err
}
})
}
const resetPasswordWithToken = async (password: string, passwordConfirmation: string, token: string, email: string) => {
await $fetch('/sanctum/csrf-cookie', {
baseURL: config.public.apiBase,
credentials: 'include',
})
await $api('/api/reset-password-token', {
method: 'POST', method: 'POST',
body: { body: {
password, password,
@ -59,6 +79,20 @@ export const useAuth = () => {
await navigateTo('/lists') await navigateTo('/lists')
} }
const forgotPassword = async (email: string) => {
await $fetch('/sanctum/csrf-cookie', {
baseURL: config.public.apiBase,
credentials: 'include',
})
await $api('/api/forgot-password', {
method: 'POST',
body: {
email
},
})
await navigateTo('/login')
}
const xsrfToken = useCookie('XSRF-TOKEN') const xsrfToken = useCookie('XSRF-TOKEN')
const logout = async () => { const logout = async () => {
@ -77,5 +111,5 @@ export const useAuth = () => {
navigateTo('/auth/login') navigateTo('/auth/login')
} }
return {login, register, resetPassword, logout} return {login, register, forgotPassword, resetPassword, resetPasswordWithToken, logout}
} }

View file

@ -2,7 +2,8 @@ export default defineNuxtRouteMiddleware((to) => {
const publicRoutes = [ const publicRoutes = [
'auth-login', 'auth-login',
'auth-register', 'auth-register',
'auth-reset-password', 'auth-reset-password-token',
'auth-forgot-password',
'invitations-token-accept', 'invitations-token-accept',
'invitations-token-decline', 'invitations-token-decline',
] ]

View file

@ -0,0 +1,35 @@
<script lang="ts" setup>
import ButtonAction from "~/components/common/button-action.vue";
definePageMeta({
layout: 'auth'
})
const {forgotPassword} = useAuth();
const email = ref("");
const handlePasswordReset = () => {
forgotPassword(email.value);
}
</script>
<template>
<div>
<form class="forgot-password-form">
<label for="email">Email</label>
<input id="email" v-model="email" type="email"/>
<ButtonAction button-text="Send Reset Link" @action="handlePasswordReset"/>
</form>
</div>
</template>
<style scoped>
form {
display: flex;
flex-direction: column;
gap: 1rem;
}
</style>

View file

@ -10,11 +10,16 @@ definePageMeta({
<div class="content"> <div class="content">
<h1>Log in</h1> <h1>Log in</h1>
<login-form/> <login-form/>
<NuxtLink class="link" to="/auth/forgot-password">Forgot Your Password?</NuxtLink>
</div> </div>
</template> </template>
<style scoped> <style scoped>
.link {
color: #007bff;
}
.content { .content {
display: flex; display: flex;
flex-direction: column; flex-direction: column;