@@ -12,13 +12,17 @@ import { GeneralErrorBoundary } from '~/components/error-boundary.tsx'
12
12
import { ErrorList , Field } from '~/components/forms.tsx'
13
13
import { StatusButton } from '~/components/ui/status-button.tsx'
14
14
import {
15
+ type VerifyFunctionArgs ,
15
16
getRedirectToUrl ,
16
17
prepareVerification ,
17
18
} from '~/routes/resources+/verify.tsx'
18
19
import { prisma } from '~/utils/db.server.ts'
19
20
import { sendEmail } from '~/utils/email.server.ts'
20
21
import { emailSchema , usernameSchema } from '~/utils/user-validation.ts'
21
22
import { ForgotPasswordEmail } from './email.server.tsx'
23
+ import { invariant , invariantResponse } from '~/utils/misc.ts'
24
+ import { commitSession , getSession } from '~/utils/session.server.ts'
25
+ import { resetPasswordUsernameSessionKey } from '../reset-password.tsx'
22
26
23
27
const ForgotPasswordSchema = z . object ( {
24
28
usernameOrEmail : z . union ( [ emailSchema , usernameSchema ] ) ,
@@ -28,20 +32,20 @@ export async function action({ request }: DataFunctionArgs) {
28
32
const formData = await request . formData ( )
29
33
const submission = await parse ( formData , {
30
34
schema : ForgotPasswordSchema . superRefine ( async ( data , ctx ) => {
31
- if ( data . usernameOrEmail . includes ( '@' ) ) return
32
-
33
- // check the username exists. Usernames have to be unique anyway so anyone
34
- // signing up can check whether a username exists by trying to sign up
35
- // with it.
36
- const user = await prisma . user . findUnique ( {
37
- where : { username : data . usernameOrEmail } ,
35
+ const user = await prisma . user . findFirst ( {
36
+ where : {
37
+ OR : [
38
+ { email : data . usernameOrEmail } ,
39
+ { username : data . usernameOrEmail } ,
40
+ ] ,
41
+ } ,
38
42
select : { id : true } ,
39
43
} )
40
44
if ( ! user ) {
41
45
ctx . addIssue ( {
42
46
path : [ 'usernameOrEmail' ] ,
43
47
code : z . ZodIssueCode . custom ,
44
- message : 'No user exists with this username' ,
48
+ message : 'No user exists with this username or email ' ,
45
49
} )
46
50
return
47
51
}
@@ -62,42 +66,54 @@ export async function action({ request }: DataFunctionArgs) {
62
66
target : usernameOrEmail ,
63
67
} )
64
68
65
- // fire, forget, and don't wait to combat timing attacks
66
- void sendVerifyEmail ( { request, target : usernameOrEmail } )
67
-
68
- return redirect ( redirectTo . toString ( ) )
69
- }
70
-
71
- async function sendVerifyEmail ( {
72
- request,
73
- target,
74
- } : {
75
- request : Request
76
- target : string
77
- } ) {
78
69
const user = await prisma . user . findFirst ( {
79
- where : { OR : [ { email : target } , { username : target } ] } ,
70
+ where : { OR : [ { email : usernameOrEmail } , { username : usernameOrEmail } ] } ,
80
71
select : { email : true , username : true } ,
81
72
} )
82
- if ( ! user ) {
83
- // maybe they're trying to see whether a user exists? We're not gonna tell them...
84
- return
85
- }
73
+ invariantResponse ( user , 'User should exist' )
86
74
87
75
const { verifyUrl, otp } = await prepareVerification ( {
88
76
period : 10 * 60 ,
89
77
request,
90
78
type : 'forgot-password' ,
91
- target,
79
+ target : usernameOrEmail ,
92
80
} )
93
81
94
- await sendEmail ( {
82
+ const response = await sendEmail ( {
95
83
to : user . email ,
96
84
subject : `Epic Notes Password Reset` ,
97
85
react : (
98
86
< ForgotPasswordEmail onboardingUrl = { verifyUrl . toString ( ) } otp = { otp } />
99
87
) ,
100
88
} )
89
+
90
+ if ( response . status === 'success' ) {
91
+ return redirect ( redirectTo . toString ( ) )
92
+ } else {
93
+ submission . error [ '' ] = response . error . message
94
+ return json ( { status : 'error' , submission } as const , { status : 500 } )
95
+ }
96
+ }
97
+
98
+ export async function handleVerification ( {
99
+ request,
100
+ submission,
101
+ } : VerifyFunctionArgs ) {
102
+ invariant ( submission . value , 'submission.value should be defined by now' )
103
+ const target = submission . value . target
104
+ const user = await prisma . user . findFirst ( {
105
+ where : { OR : [ { email : target } , { username : target } ] } ,
106
+ select : { email : true , username : true } ,
107
+ } )
108
+ // we don't want to say the user is not found if the email is not found
109
+ // because that would allow an attacker to check if an email is registered
110
+ invariantResponse ( user , 'Invalid code' )
111
+
112
+ const session = await getSession ( request . headers . get ( 'cookie' ) )
113
+ session . set ( resetPasswordUsernameSessionKey , user . username )
114
+ return redirect ( '/reset-password' , {
115
+ headers : { 'Set-Cookie' : await commitSession ( session ) } ,
116
+ } )
101
117
}
102
118
103
119
export const meta : V2_MetaFunction = ( ) => {
0 commit comments