Skip to content

Commit e9a90b6

Browse files
committed
feat: handle forgot password
1 parent bd710cd commit e9a90b6

File tree

3 files changed

+120
-13
lines changed

3 files changed

+120
-13
lines changed

src/runtime/components/NUsersResetPasswordForm.vue

Lines changed: 116 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,19 @@
11
<script setup lang="ts">
2-
import { ref, watch } from 'vue'
3-
import type { UserWithoutPassword, ModuleOptions, ResetPasswordFormProps } from '#nuxt-users/types'
2+
/**
3+
* NUsersResetPasswordForm Component
4+
*
5+
* A dual-purpose password form component that handles both:
6+
* 1. Password reset from email links (when token and email are in URL query params)
7+
* 2. Password change for logged-in users (when no token/email in URL)
8+
*
9+
* The component automatically detects which mode to use based on URL parameters.
10+
* For password reset: requires token and email in URL query params
11+
* For password change: requires user to be logged in and provide current password
12+
*/
13+
import { ref, watch, computed } from 'vue'
14+
import type { UserWithoutPassword, ModuleOptions, ResetPasswordFormProps } from 'nuxt-users/utils'
415
import { usePasswordValidation } from '../composables/usePasswordValidation'
5-
import { useRuntimeConfig } from '#imports'
16+
import { useRuntimeConfig, useRoute, useRouter } from '#imports'
617
import NUsersPasswordStrengthIndicator from './NUsersPasswordStrengthIndicator.vue'
718
819
interface Emits {
@@ -17,10 +28,22 @@ const moduleOptions = nuxtUsers as ModuleOptions
1728
const props = defineProps<ResetPasswordFormProps>()
1829
const emit = defineEmits<Emits>()
1930
31+
const route = useRoute()
32+
const router = useRouter()
33+
2034
const isPasswordLoading = ref(false)
2135
const passwordError = ref('')
2236
const passwordSuccess = ref('')
2337
38+
// Check if this is a password reset from email link
39+
const isPasswordReset = computed(() => {
40+
return route.query.token && route.query.email
41+
})
42+
43+
// Get token and email from URL for password reset
44+
const resetToken = computed(() => route.query.token as string)
45+
const resetEmail = computed(() => route.query.email as string)
46+
2447
// Password form data
2548
const passwordForm = ref({
2649
currentPassword: '',
@@ -41,7 +64,7 @@ watch(() => passwordForm.value.newPassword, (newPassword) => {
4164
}
4265
})
4366
44-
// Update password
67+
// Update password (for logged-in users)
4568
const updatePassword = async () => {
4669
isPasswordLoading.value = true
4770
passwordError.value = ''
@@ -76,16 +99,94 @@ const updatePassword = async () => {
7699
isPasswordLoading.value = false
77100
}
78101
}
102+
103+
// Reset password (for email reset links)
104+
const resetPassword = async () => {
105+
if (!resetToken.value || !resetEmail.value) {
106+
passwordError.value = 'Missing reset token or email. Please use the link from your email.'
107+
return
108+
}
109+
110+
if (passwordForm.value.newPassword !== passwordForm.value.newPasswordConfirmation) {
111+
passwordError.value = 'Passwords do not match'
112+
return
113+
}
114+
115+
isPasswordLoading.value = true
116+
passwordError.value = ''
117+
passwordSuccess.value = ''
118+
119+
try {
120+
await $fetch(props.resetPasswordEndpoint || nuxtUsers.apiBasePath + '/password/reset', {
121+
method: 'POST',
122+
body: {
123+
token: resetToken.value,
124+
email: resetEmail.value,
125+
password: passwordForm.value.newPassword,
126+
password_confirmation: passwordForm.value.newPasswordConfirmation
127+
}
128+
})
129+
130+
passwordSuccess.value = 'Password reset successfully. You can now log in with your new password.'
131+
emit('password-updated')
132+
133+
// Clear form
134+
passwordForm.value = {
135+
currentPassword: '',
136+
newPassword: '',
137+
newPasswordConfirmation: ''
138+
}
139+
140+
// Redirect to login page after a short delay
141+
setTimeout(() => {
142+
router.push(props.redirectTo || '/login')
143+
}, 2000)
144+
}
145+
catch (err: unknown) {
146+
const errorMessage = err && typeof err === 'object' && 'data' in err && err.data && typeof err.data === 'object' && 'statusMessage' in err.data
147+
? String(err.data.statusMessage)
148+
: err instanceof Error
149+
? err.message
150+
: 'Failed to reset password'
151+
passwordError.value = errorMessage
152+
emit('password-error', errorMessage)
153+
}
154+
finally {
155+
isPasswordLoading.value = false
156+
}
157+
}
158+
159+
// Handle form submission based on mode
160+
const handleSubmit = () => {
161+
if (isPasswordReset.value) {
162+
resetPassword()
163+
}
164+
else {
165+
updatePassword()
166+
}
167+
}
79168
</script>
80169

81170
<template>
82171
<div class="n-users-section">
83172
<h2 class="n-users-section-header">
84-
Change Password
173+
{{ isPasswordReset ? 'Reset Password' : 'Change Password' }}
85174
</h2>
86175

87-
<form @submit.prevent="updatePassword">
88-
<div class="n-users-form-group">
176+
<div
177+
v-if="isPasswordReset"
178+
class="n-users-info-message"
179+
>
180+
<p>You are resetting your password using a secure link.</p>
181+
<p><strong>Email:</strong> {{ resetEmail }}</p>
182+
</div>
183+
184+
<form @submit.prevent="handleSubmit">
185+
<!-- Current Password field - only show for logged-in users -->
186+
<div
187+
v-if="!isPasswordReset"
188+
class="n-users-form-group"
189+
>
89190
<label
90191
for="currentPassword"
91192
class="n-users-form-label"
@@ -104,7 +205,7 @@ const updatePassword = async () => {
104205
<label
105206
for="newPassword"
106207
class="n-users-form-label"
107-
>New Password</label>
208+
>{{ isPasswordReset ? 'New Password' : 'New Password' }}</label>
108209
<input
109210
id="newPassword"
110211
v-model="passwordForm.newPassword"
@@ -134,7 +235,7 @@ const updatePassword = async () => {
134235
<label
135236
for="newPasswordConfirmation"
136237
class="n-users-form-label"
137-
>Confirm New Password</label>
238+
>Confirm {{ isPasswordReset ? 'New Password' : 'New Password' }}</label>
138239
<input
139240
id="newPasswordConfirmation"
140241
v-model="passwordForm.newPasswordConfirmation"
@@ -164,8 +265,12 @@ const updatePassword = async () => {
164265
class="n-users-btn n-users-btn-primary"
165266
:disabled="isPasswordLoading"
166267
>
167-
<span v-if="isPasswordLoading">Updating...</span>
168-
<span v-else>Update Password</span>
268+
<span v-if="isPasswordLoading">
269+
{{ isPasswordReset ? 'Resetting...' : 'Updating...' }}
270+
</span>
271+
<span v-else>
272+
{{ isPasswordReset ? 'Reset Password' : 'Update Password' }}
273+
</span>
169274
</button>
170275
</form>
171276
</div>

src/runtime/utils/permissions.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import type { Permission } from '#nuxt-users/types'
1+
import type { Permission } from 'nuxt-users/utils'
22

33
/**
44
* Checks if a path matches a pattern (supports wildcards)
@@ -88,7 +88,7 @@ export const hasPermission = (
8888
}
8989

9090
// Check if the method is allowed (case-insensitive)
91-
return permission.methods.some(allowedMethod => allowedMethod.toUpperCase() === method.toUpperCase())
91+
return permission.methods.some((allowedMethod: string) => allowedMethod.toUpperCase() === method.toUpperCase())
9292
}
9393

9494
return false

src/types.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -205,6 +205,8 @@ export interface LogoutLinkProps {
205205
export interface ResetPasswordFormProps {
206206
apiEndpoint?: string
207207
updatePasswordEndpoint?: string
208+
resetPasswordEndpoint?: string
209+
redirectTo?: string
208210
}
209211

210212
export interface DisplayFieldsProps {

0 commit comments

Comments
 (0)