1
1
import { ArrowLeftIcon , EnvelopeIcon } from "@heroicons/react/20/solid" ;
2
2
import { InboxArrowDownIcon } from "@heroicons/react/24/solid" ;
3
- import type { ActionFunctionArgs , LoaderFunctionArgs , MetaFunction } from "@remix-run/node" ;
4
- import { redirect } from "@remix-run/node" ;
3
+ import {
4
+ redirect ,
5
+ type ActionFunctionArgs ,
6
+ type LoaderFunctionArgs ,
7
+ type MetaFunction ,
8
+ } from "@remix-run/node" ;
5
9
import { Form , useNavigation } from "@remix-run/react" ;
6
10
import { typedjson , useTypedLoaderData } from "remix-typedjson" ;
7
11
import { z } from "zod" ;
@@ -18,6 +22,13 @@ import { Spinner } from "~/components/primitives/Spinner";
18
22
import { TextLink } from "~/components/primitives/TextLink" ;
19
23
import { authenticator } from "~/services/auth.server" ;
20
24
import { commitSession , getUserSession } from "~/services/sessionStorage.server" ;
25
+ import {
26
+ checkMagicLinkEmailRateLimit ,
27
+ checkMagicLinkEmailDailyRateLimit ,
28
+ MagicLinkRateLimitError ,
29
+ checkMagicLinkIpRateLimit ,
30
+ } from "~/services/magicLinkRateLimiter.server" ;
31
+ import { logger , tryCatch } from "@trigger.dev/core/v3" ;
21
32
22
33
export const meta : MetaFunction = ( { matches } ) => {
23
34
const parentMeta = matches
@@ -71,26 +82,81 @@ export async function action({ request }: ActionFunctionArgs) {
71
82
72
83
const payload = Object . fromEntries ( await clonedRequest . formData ( ) ) ;
73
84
74
- const { action } = z
75
- . object ( {
76
- action : z . enum ( [ "send" , "reset" ] ) ,
77
- } )
85
+ const data = z
86
+ . discriminatedUnion ( "action" , [
87
+ z . object ( {
88
+ action : z . literal ( "send" ) ,
89
+ email : z . string ( ) . trim ( ) . toLowerCase ( ) ,
90
+ } ) ,
91
+ z . object ( {
92
+ action : z . literal ( "reset" ) ,
93
+ } ) ,
94
+ ] )
78
95
. parse ( payload ) ;
79
96
80
- if ( action === "send" ) {
81
- return authenticator . authenticate ( "email-link" , request , {
82
- successRedirect : "/login/magic" ,
83
- failureRedirect : "/login/magic" ,
84
- } ) ;
85
- } else {
86
- const session = await getUserSession ( request ) ;
87
- session . unset ( "triggerdotdev:magiclink" ) ;
88
-
89
- return redirect ( "/login/magic" , {
90
- headers : {
91
- "Set-Cookie" : await commitSession ( session ) ,
92
- } ,
93
- } ) ;
97
+ switch ( data . action ) {
98
+ case "send" : {
99
+ const { email } = data ;
100
+ const clientIp = request . headers . get ( "x-forwarded-for" ) ;
101
+
102
+ const [ error ] = await tryCatch (
103
+ Promise . all ( [
104
+ clientIp ? checkMagicLinkIpRateLimit ( clientIp ) : Promise . resolve ( ) ,
105
+ checkMagicLinkEmailRateLimit ( email ) ,
106
+ checkMagicLinkEmailDailyRateLimit ( email ) ,
107
+ ] )
108
+ ) ;
109
+
110
+ if ( error ) {
111
+ if ( error instanceof MagicLinkRateLimitError ) {
112
+ logger . warn ( "Login magic link rate limit exceeded" , {
113
+ clientIp,
114
+ email,
115
+ error,
116
+ } ) ;
117
+ } else {
118
+ logger . error ( "Failed sending login magic link" , {
119
+ clientIp,
120
+ email,
121
+ error,
122
+ } ) ;
123
+ }
124
+
125
+ const errorMessage =
126
+ error instanceof MagicLinkRateLimitError
127
+ ? "Failed sending magic link. Please try again shortly."
128
+ : "Too many magic link requests. Please try again shortly." ;
129
+
130
+ const session = await getUserSession ( request ) ;
131
+ session . set ( "auth:error" , {
132
+ message : errorMessage ,
133
+ } ) ;
134
+
135
+ return redirect ( "/login/magic" , {
136
+ headers : {
137
+ "Set-Cookie" : await commitSession ( session ) ,
138
+ } ,
139
+ } ) ;
140
+ }
141
+
142
+ return authenticator . authenticate ( "email-link" , request , {
143
+ successRedirect : "/login/magic" ,
144
+ failureRedirect : "/login/magic" ,
145
+ } ) ;
146
+ }
147
+ case "reset" :
148
+ default : {
149
+ data . action satisfies "reset" ;
150
+
151
+ const session = await getUserSession ( request ) ;
152
+ session . unset ( "triggerdotdev:magiclink" ) ;
153
+
154
+ return redirect ( "/login/magic" , {
155
+ headers : {
156
+ "Set-Cookie" : await commitSession ( session ) ,
157
+ } ,
158
+ } ) ;
159
+ }
94
160
}
95
161
}
96
162
0 commit comments