@@ -15,7 +15,10 @@ import { InputOTP, InputOTPGroup, InputOTPSlot } from "~/components/primitives/I
1515import { Paragraph } from "~/components/primitives/Paragraph" ;
1616import { Spinner } from "~/components/primitives/Spinner" ;
1717import { authenticator } from "~/services/auth.server" ;
18- import { commitSession , getUserSession } from "~/services/sessionStorage.server" ;
18+ import { commitSession , getUserSession , sessionStorage } from "~/services/sessionStorage.server" ;
19+ import { MultiFactorAuthenticationService } from "~/services/mfa/multiFactorAuthentication.server" ;
20+ import { redirectWithErrorMessage } from "~/models/message.server" ;
21+ import { ServiceValidationError } from "~/v3/services/baseService.server" ;
1922
2023export const meta : MetaFunction = ( { matches } ) => {
2124 const parentMeta = matches
@@ -37,11 +40,20 @@ export const meta: MetaFunction = ({ matches }) => {
3740} ;
3841
3942export async function loader ( { request } : LoaderFunctionArgs ) {
43+ // Check if user is already fully authenticated
4044 await authenticator . isAuthenticated ( request , {
4145 successRedirect : "/" ,
4246 } ) ;
4347
4448 const session = await getUserSession ( request ) ;
49+
50+ // Check if there's a pending MFA user ID
51+ const pendingUserId = session . get ( "pending-mfa-user-id" ) ;
52+ if ( ! pendingUserId ) {
53+ // No pending MFA, redirect to login
54+ return redirect ( "/login" ) ;
55+ }
56+
4557 const error = session . get ( "auth:error" ) ;
4658
4759 let mfaError : string | undefined ;
@@ -64,42 +76,100 @@ export async function loader({ request }: LoaderFunctionArgs) {
6476}
6577
6678export async function action ( { request } : ActionFunctionArgs ) {
67- const clonedRequest = request . clone ( ) ;
79+ try {
80+ const session = await getUserSession ( request ) ;
81+ const pendingUserId = session . get ( "pending-mfa-user-id" ) ;
82+
83+ if ( ! pendingUserId ) {
84+ return redirect ( "/login" ) ;
85+ }
6886
69- const payload = Object . fromEntries ( await clonedRequest . formData ( ) ) ;
87+ const payload = Object . fromEntries ( await request . formData ( ) ) ;
7088
71- const { action } = z
72- . object ( {
73- action : z . enum ( [ "verify-recovery" , "verify-mfa" ] ) ,
74- } )
75- . parse ( payload ) ;
89+ const { action } = z
90+ . object ( {
91+ action : z . enum ( [ "verify-recovery" , "verify-mfa" ] ) ,
92+ } )
93+ . parse ( payload ) ;
7694
77- if ( action === "verify-recovery" ) {
78- // TODO: Implement recovery code verification logic
79- const recoveryCode = payload . recoveryCode ;
95+ const mfaService = new MultiFactorAuthenticationService ( ) ;
8096
81- // For now, just redirect to dashboard
82- return redirect ( "/" ) ;
83- } else if ( action === "verify-mfa" ) {
84- // TODO: Implement MFA code verification logic
85- const mfaCode = payload . mfaCode ;
97+ if ( action === "verify-recovery" ) {
98+ const recoveryCode = payload . recoveryCode as string ;
99+
100+ if ( ! recoveryCode ) {
101+ session . set ( "auth:error" , { message : "Recovery code is required" } ) ;
102+ return redirect ( "/login/mfa" , {
103+ headers : { "Set-Cookie" : await commitSession ( session ) } ,
104+ } ) ;
105+ }
86106
87- // For now, just redirect to dashboard
88- return redirect ( "/" ) ;
89- } else {
90- const session = await getUserSession ( request ) ;
91- session . unset ( "triggerdotdev:magiclink" ) ;
107+ const result = await mfaService . verifyRecoveryCodeForLogin ( pendingUserId , recoveryCode ) ;
108+
109+ if ( ! result . success ) {
110+ session . set ( "auth:error" , { message : result . error } ) ;
111+ return redirect ( "/login/mfa" , {
112+ headers : { "Set-Cookie" : await commitSession ( session ) } ,
113+ } ) ;
114+ }
92115
93- return redirect ( "/login/magic" , {
94- headers : {
95- "Set-Cookie" : await commitSession ( session ) ,
96- } ,
97- } ) ;
116+ // Recovery code verified - complete the login
117+ return await completeLogin ( request , session , pendingUserId ) ;
118+
119+ } else if ( action === "verify-mfa" ) {
120+ const mfaCode = payload . mfaCode as string ;
121+
122+ if ( ! mfaCode || mfaCode . length !== 6 ) {
123+ session . set ( "auth:error" , { message : "Valid 6-digit code is required" } ) ;
124+ return redirect ( "/login/mfa" , {
125+ headers : { "Set-Cookie" : await commitSession ( session ) } ,
126+ } ) ;
127+ }
128+
129+ const result = await mfaService . verifyTotpForLogin ( pendingUserId , mfaCode ) ;
130+
131+ if ( ! result . success ) {
132+ session . set ( "auth:error" , { message : result . error } ) ;
133+ return redirect ( "/login/mfa" , {
134+ headers : { "Set-Cookie" : await commitSession ( session ) } ,
135+ } ) ;
136+ }
137+
138+ // TOTP code verified - complete the login
139+ return await completeLogin ( request , session , pendingUserId ) ;
140+ }
141+
142+ return redirect ( "/login" ) ;
143+
144+ } catch ( error ) {
145+ if ( error instanceof ServiceValidationError ) {
146+ return redirectWithErrorMessage ( "/login" , request , error . message ) ;
147+ }
148+ throw error ;
98149 }
99150}
100151
152+ async function completeLogin ( request : Request , session : any , userId : string ) {
153+ // Create a new authenticated session
154+ const authSession = await sessionStorage . getSession ( request . headers . get ( "Cookie" ) ) ;
155+ authSession . set ( authenticator . sessionKey , { userId } ) ;
156+
157+ // Get the redirect URL and clean up pending MFA data
158+ const redirectTo = session . get ( "pending-mfa-redirect-to" ) ?? "/" ;
159+ session . unset ( "pending-mfa-user-id" ) ;
160+ session . unset ( "pending-mfa-redirect-to" ) ;
161+ session . unset ( "auth:error" ) ;
162+
163+ return redirect ( redirectTo , {
164+ headers : {
165+ "Set-Cookie" : await sessionStorage . commitSession ( authSession ) ,
166+ } ,
167+ } ) ;
168+ }
169+
101170export default function LoginMfaPage ( ) {
102- const { mfaError } = useTypedLoaderData < typeof loader > ( ) ;
171+ const data = useTypedLoaderData < typeof loader > ( ) ;
172+ const mfaError = 'mfaError' in data ? data . mfaError : undefined ;
103173 const navigate = useNavigation ( ) ;
104174 const [ showRecoveryCode , setShowRecoveryCode ] = useState ( false ) ;
105175 const [ mfaCode , setMfaCode ] = useState ( "" ) ;
@@ -151,7 +221,7 @@ export default function LoginMfaPage() {
151221 < span className = "text-text-bright" > Verify</ span >
152222 ) }
153223 </ Button >
154- { mfaError && < FormError > { mfaError } </ FormError > }
224+ { typeof mfaError === 'string' && < FormError > { mfaError } </ FormError > }
155225 </ Fieldset >
156226 < Button
157227 type = "button"
@@ -203,7 +273,7 @@ export default function LoginMfaPage() {
203273 < span className = "text-text-bright" > Verify</ span >
204274 ) }
205275 </ Button >
206- { mfaError && < FormError > { mfaError } </ FormError > }
276+ { typeof mfaError === 'string' && < FormError > { mfaError } </ FormError > }
207277 </ Fieldset >
208278 < Button
209279 type = "button"
0 commit comments