Merge pull request 'account-page-password-reset' (#14) from account-page-password-reset into main

Reviewed-on: #14
This commit is contained in:
Edward Tirado Jr 2026-04-20 20:10:22 +00:00
commit b782494de7
9 changed files with 128 additions and 13 deletions

View file

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

View file

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

View file

@ -1,18 +1,49 @@
<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>
<template>
<form class="password-form">
<form class="password-form" @submit.prevent>
<div class="form-group">
<label for="current-password">Current Password</label>
<input id="current-password" type="password"/>
<label for="new-password">Current Password</label>
<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 class="form-group">
<label for="new-password">New Password</label>
<input id="new-password" type="password"/>
</div>
<ButtonAction button-text="Reset Password" @action="handlePasswordReset"/>
<ul v-if="errors.length" class="error-message">
<li v-for="msg in errors" :key="msg">{{ msg }}</li>
</ul>
<p v-if="successMessage" class="success-message">{{ successMessage }}</p>
</form>
</template>
@ -22,4 +53,12 @@
flex-direction: column;
gap: 1rem;
}
.error-message {
color: var(--color-error-text);
}
.success-message {
color: var(--color-success-text);
}
</style>

View file

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

View file

@ -37,12 +37,32 @@ export const useAuth = () => {
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', {
baseURL: config.public.apiBase,
credentials: 'include',
})
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',
body: {
password,
@ -59,6 +79,20 @@ export const useAuth = () => {
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 logout = async () => {
@ -77,5 +111,5 @@ export const useAuth = () => {
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 = [
'auth-login',
'auth-register',
'auth-reset-password',
'auth-reset-password-token',
'auth-forgot-password',
'invitations-token-accept',
'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">
<h1>Log in</h1>
<login-form/>
<NuxtLink class="link" to="/auth/forgot-password">Forgot Your Password?</NuxtLink>
</div>
</template>
<style scoped>
.link {
color: #007bff;
}
.content {
display: flex;
flex-direction: column;