@@ -12,18 +12,27 @@ import { safeRedirect } from 'remix-utils'
12
12
import { z } from 'zod'
13
13
import { CheckboxField , ErrorList , Field } from '~/components/forms.tsx'
14
14
import { StatusButton } from '~/components/ui/status-button.tsx'
15
- import { authenticator } from '~/utils/auth.server.ts'
15
+ import { authenticator , getUserId } from '~/utils/auth.server.ts'
16
16
import { prisma } from '~/utils/db.server.ts'
17
17
import { redirectWithToast } from '~/utils/flash-session.server.ts'
18
- import { EnsurePE , ensurePE , getReferrerRoute } from '~/utils/misc.tsx'
18
+ import {
19
+ EnsurePE ,
20
+ ensurePE ,
21
+ getReferrerRoute ,
22
+ invariant ,
23
+ } from '~/utils/misc.tsx'
19
24
import {
20
25
commitSession ,
21
26
destroySession ,
22
27
getSession ,
23
28
} from '~/utils/session.server.ts'
24
29
import { passwordSchema , usernameSchema } from '~/utils/user-validation.ts'
25
30
import { checkboxSchema } from '~/utils/zod-extensions.ts'
26
- import { isCodeValid } from './verify.tsx'
31
+ import {
32
+ type VerificationTypes ,
33
+ isCodeValid ,
34
+ type VerifyFunctionArgs ,
35
+ } from './verify.tsx'
27
36
28
37
const ROUTE_PATH = '/resources/login'
29
38
@@ -51,19 +60,58 @@ const LoginFormSchema = z.object({
51
60
remember : checkboxSchema ( ) ,
52
61
} )
53
62
63
+ const verifiedTimeKey = 'verified-time'
54
64
const unverifiedSessionIdKey = 'unverified-session-id'
55
65
const loginSubmissionKey = 'login-submission'
66
+ const inlineLoginFormId = 'inline-login'
67
+ const inlineTwoFAFormId = 'inline-two-fa'
68
+ const verificationType = '2fa' satisfies VerificationTypes
56
69
57
- async function shouldRequestTwoFA ( request : Request ) {
58
- const session = await getSession ( request . headers . get ( 'cookie' ) )
59
- return session . has ( unverifiedSessionIdKey )
70
+ export async function handleVerification ( {
71
+ request,
72
+ body,
73
+ submission,
74
+ } : VerifyFunctionArgs ) {
75
+ invariant ( submission . value , 'Submission should have a value by this point' )
76
+ const cookieSession = await getSession ( request . headers . get ( 'cookie' ) )
77
+ const { redirectTo } = submission . value
78
+ cookieSession . set ( verifiedTimeKey , Date . now ( ) )
79
+ const responseInit = {
80
+ headers : { 'Set-Cookie' : await commitSession ( cookieSession ) } ,
81
+ }
82
+
83
+ if ( redirectTo ) {
84
+ return redirect ( safeRedirect ( redirectTo ) , responseInit )
85
+ } else {
86
+ ensurePE ( body , request , responseInit )
87
+ return json ( { status : 'success' , submission } as const , responseInit )
88
+ }
89
+ }
90
+
91
+ export async function shouldRequestTwoFA ( request : Request ) {
92
+ const cookieSession = await getSession ( request . headers . get ( 'cookie' ) )
93
+ if ( cookieSession . has ( unverifiedSessionIdKey ) ) return true
94
+ const userId = await getUserId ( request )
95
+ if ( ! userId ) return false
96
+ // if it's over two hours since they last verified, we should request 2FA again
97
+ const userHasTwoFA = await prisma . verification . findUnique ( {
98
+ select : { id : true } ,
99
+ where : { target_type : { target : userId , type : verificationType } } ,
100
+ } )
101
+ if ( ! userHasTwoFA ) return false
102
+ const verifiedTime = cookieSession . get ( verifiedTimeKey ) ?? new Date ( 0 )
103
+ const twoHours = 1000 * 60 * 60 * 2
104
+ return Date . now ( ) - verifiedTime > twoHours
60
105
}
61
106
62
107
export async function action ( args : DataFunctionArgs ) {
63
- if ( await shouldRequestTwoFA ( args . request ) ) {
108
+ const form = ( await args . request . clone ( ) . formData ( ) ) . get ( 'form' )
109
+ if ( form === inlineTwoFAFormId ) {
64
110
return inlineTwoFAAction ( args )
65
- } else {
111
+ } else if ( form === inlineLoginFormId ) {
66
112
return inlineLoginAction ( args )
113
+ } else {
114
+ throw new Response ( 'Invalid form' , { status : 400 } )
67
115
}
68
116
}
69
117
@@ -134,7 +182,7 @@ async function inlineLoginAction({ request }: DataFunctionArgs) {
134
182
const { remember, redirectTo, session } = submission . value
135
183
136
184
const verification = await prisma . verification . findUnique ( {
137
- where : { target_type : { target : session . userId , type : '2fa' } } ,
185
+ where : { target_type : { target : session . userId , type : verificationType } } ,
138
186
select : { id : true } ,
139
187
} )
140
188
const userHasTwoFactor = Boolean ( verification )
@@ -192,7 +240,7 @@ function InlineLoginForm({
192
240
const loginFetcher = useFetcher < typeof inlineLoginAction > ( )
193
241
194
242
const [ form , fields ] = useForm ( {
195
- id : 'inline-login' ,
243
+ id : inlineLoginFormId ,
196
244
defaultValue : { redirectTo } ,
197
245
constraint : getFieldsetConstraint ( LoginFormSchema ) ,
198
246
lastSubmission : loginFetcher . data ?. submission ?? submission ,
@@ -211,6 +259,7 @@ function InlineLoginForm({
211
259
name = "login"
212
260
{ ...form . props }
213
261
>
262
+ < input type = "hidden" name = "form" value = { form . id } />
214
263
< EnsurePE />
215
264
< Field
216
265
labelProps = { { children : 'Username' } }
@@ -316,7 +365,7 @@ async function inlineTwoFAAction({ request }: DataFunctionArgs) {
316
365
schema : TwoFAFormSchema . superRefine ( async ( data , ctx ) => {
317
366
const codeIsValid = await isCodeValid ( {
318
367
code : data . code ,
319
- type : '2fa' ,
368
+ type : verificationType ,
320
369
target : session . userId ,
321
370
} )
322
371
if ( ! codeIsValid ) {
@@ -351,6 +400,7 @@ async function inlineTwoFAAction({ request }: DataFunctionArgs) {
351
400
}
352
401
353
402
const { redirectTo } = submission . value
403
+ cookieSession . set ( verifiedTimeKey , Date . now ( ) )
354
404
cookieSession . unset ( unverifiedSessionIdKey )
355
405
cookieSession . set ( authenticator . sessionKey , session . id )
356
406
const responseInit = {
@@ -377,7 +427,7 @@ function InlineTwoFA({
377
427
const twoFAFetcher = useFetcher < typeof inlineTwoFAAction > ( )
378
428
379
429
const [ form , fields ] = useForm ( {
380
- id : 'inline-two-fa' ,
430
+ id : inlineTwoFAFormId ,
381
431
defaultValue : { redirectTo } ,
382
432
constraint : getFieldsetConstraint ( TwoFAFormSchema ) ,
383
433
lastSubmission : twoFAFetcher . data ?. submission ?? submission ,
@@ -389,6 +439,7 @@ function InlineTwoFA({
389
439
390
440
return (
391
441
< twoFAFetcher . Form method = "POST" action = { ROUTE_PATH } { ...form . props } >
442
+ < input type = "hidden" name = "form" value = { form . id } />
392
443
{ /* Putting this at the top so we can have the tab order of cancel first,
393
444
but have "enter" submit the confirmation. */ }
394
445
< button type = "submit" className = "hidden" name = "intent" value = "confirm" />
0 commit comments