Skip to content

Commit c36adf5

Browse files
committed
Use flash message for errors on password reset page
1 parent abea7dd commit c36adf5

File tree

3 files changed

+82
-34
lines changed

3 files changed

+82
-34
lines changed

demos/bookstore/app/auth.test.ts

Lines changed: 67 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -135,4 +135,71 @@ describe('auth handlers', () => {
135135
assertContains(html, 'Invalid email or password')
136136
assertContains(html, 'returnTo=' + encodeURIComponent('/checkout'))
137137
})
138+
139+
it('POST /reset-password with mismatched passwords redirects back with error', async () => {
140+
// First, request a password reset to get a token
141+
let forgotPasswordResponse = await router.fetch('https://remix.run/forgot-password', {
142+
method: 'POST',
143+
body: new URLSearchParams({
144+
145+
}),
146+
})
147+
148+
let html = await forgotPasswordResponse.text()
149+
// Extract token from the reset link in the demo response
150+
let tokenMatch = html.match(/\/reset-password\/([^"]+)/)
151+
assert.ok(tokenMatch, 'Expected to find reset token in response')
152+
let token = tokenMatch[1]
153+
154+
// Try to reset password with mismatched passwords
155+
let response = await router.fetch(`https://remix.run/reset-password/${token}`, {
156+
method: 'POST',
157+
body: new URLSearchParams({
158+
password: 'newpassword123',
159+
confirmPassword: 'differentpassword',
160+
}),
161+
redirect: 'manual',
162+
})
163+
164+
assert.equal(response.status, 302)
165+
assert.equal(response.headers.get('Location'), `/reset-password/${token}`)
166+
167+
// Follow redirect to see the error message
168+
let sessionCookie = getSessionCookie(response)
169+
let followUpResponse = await router.fetch(`https://remix.run/reset-password/${token}`, {
170+
headers: {
171+
Cookie: `session=${sessionCookie}`,
172+
},
173+
})
174+
175+
let errorHtml = await followUpResponse.text()
176+
assertContains(errorHtml, 'Passwords do not match')
177+
})
178+
179+
it('POST /reset-password with invalid token redirects back with error', async () => {
180+
let invalidToken = 'invalid-token-12345'
181+
182+
let response = await router.fetch(`https://remix.run/reset-password/${invalidToken}`, {
183+
method: 'POST',
184+
body: new URLSearchParams({
185+
password: 'newpassword123',
186+
confirmPassword: 'newpassword123',
187+
}),
188+
redirect: 'manual',
189+
})
190+
191+
assert.equal(response.status, 302)
192+
assert.equal(response.headers.get('Location'), `/reset-password/${invalidToken}`)
193+
194+
// Follow redirect to see the error message
195+
let sessionCookie = getSessionCookie(response)
196+
let followUpResponse = await router.fetch(`https://remix.run/reset-password/${invalidToken}`, {
197+
headers: {
198+
Cookie: `session=${sessionCookie}`,
199+
},
200+
})
201+
202+
let errorHtml = await followUpResponse.text()
203+
assertContains(errorHtml, 'Invalid or expired reset token')
204+
})
138205
})

demos/bookstore/app/auth.tsx

Lines changed: 14 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -78,9 +78,9 @@ export default {
7878
async action({ session, formData, url }) {
7979
let email = formData.get('email')?.toString() ?? ''
8080
let password = formData.get('password')?.toString() ?? ''
81-
let user = authenticateUser(email, password)
8281
let returnTo = url.searchParams.get('returnTo')
8382

83+
let user = authenticateUser(email, password)
8484
if (!user) {
8585
session.flash('error', 'Invalid email or password. Please try again.')
8686
return redirect(routes.auth.login.index.href(undefined, { returnTo }))
@@ -239,15 +239,22 @@ export default {
239239
},
240240

241241
resetPassword: {
242-
index({ params }) {
242+
index({ params, session }) {
243243
let token = params.token
244+
let error = session.get('error')
244245

245246
return render(
246247
<Document>
247248
<div class="card" style="max-width: 500px; margin: 2rem auto;">
248249
<h1>Reset Password</h1>
249250
<p>Enter your new password below.</p>
250251

252+
{typeof error === 'string' ? (
253+
<div class="alert alert-error" style="margin-bottom: 1.5rem;">
254+
{error}
255+
</div>
256+
) : null}
257+
251258
<form method="POST" action={routes.auth.resetPassword.action.href({ token })}>
252259
<div class="form-group">
253260
<label for="password">New Password</label>
@@ -280,45 +287,20 @@ export default {
280287
)
281288
},
282289

283-
async action({ formData, params }) {
290+
async action({ session, formData, params }) {
284291
let password = formData.get('password')?.toString() ?? ''
285292
let confirmPassword = formData.get('confirmPassword')?.toString() ?? ''
286293

287294
if (password !== confirmPassword) {
288-
return render(
289-
<Document>
290-
<div class="card" style="max-width: 500px; margin: 2rem auto;">
291-
<div class="alert alert-error">Passwords do not match.</div>
292-
<p>
293-
<a
294-
href={routes.auth.resetPassword.index.href({ token: params.token })}
295-
class="btn"
296-
>
297-
Try Again
298-
</a>
299-
</p>
300-
</div>
301-
</Document>,
302-
{ status: 400 },
303-
)
295+
session.flash('error', 'Passwords do not match.')
296+
return redirect(routes.auth.resetPassword.index.href({ token: params.token }))
304297
}
305298

306299
let success = resetPassword(params.token, password)
307300

308301
if (!success) {
309-
return render(
310-
<Document>
311-
<div class="card" style="max-width: 500px; margin: 2rem auto;">
312-
<div class="alert alert-error">Invalid or expired reset token.</div>
313-
<p>
314-
<a href={routes.auth.forgotPassword.index.href()} class="btn">
315-
Request New Link
316-
</a>
317-
</p>
318-
</div>
319-
</Document>,
320-
{ status: 400 },
321-
)
302+
session.flash('error', 'Invalid or expired reset token.')
303+
return redirect(routes.auth.resetPassword.index.href({ token: params.token }))
322304
}
323305

324306
return render(

demos/bookstore/app/utils/context.ts

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,8 @@
11
import { createStorageKey } from '@remix-run/fetch-router'
22
import { getContext } from '@remix-run/fetch-router/async-context-middleware'
33

4+
import { type Cart, getCart } from '../models/cart.ts'
45
import type { User } from '../models/users.ts'
5-
import type { Cart } from '../models/cart.ts'
6-
import { getCart } from '../models/cart.ts'
76

87
// Storage key for attaching user data to request context
98
let USER_KEY = createStorageKey<User>()

0 commit comments

Comments
 (0)