11import { ArrowLeftIcon } from "@heroicons/react/20/solid" ;
22import { InboxArrowDownIcon } from "@heroicons/react/24/solid" ;
33import {
4+ createCookie ,
45 redirect ,
56 type ActionFunctionArgs ,
67 type LoaderFunctionArgs ,
@@ -35,6 +36,17 @@ import { logger, tryCatch } from "@trigger.dev/core/v3";
3536import { env } from "~/env.server" ;
3637import { extractClientIp } from "~/utils/extractClientIp.server" ;
3738
39+ // The submitted email is carried to the confirmation screen in a short-lived,
40+ // httpOnly cookie rather than the URL, so the address never lands in access
41+ // logs, browser history, or error-tracker breadcrumbs.
42+ const magicLinkEmailCookie = createCookie ( "magiclink-email" , {
43+ maxAge : 60 * 10 ,
44+ httpOnly : true ,
45+ sameSite : "lax" ,
46+ secure : env . NODE_ENV === "production" ,
47+ path : "/" ,
48+ } ) ;
49+
3850export const meta : MetaFunction = ( { matches } ) => {
3951 const parentMeta = matches
4052 . flatMap ( ( match ) => match . meta ?? [ ] )
@@ -65,18 +77,21 @@ export async function loader({ request }: LoaderFunctionArgs) {
6577 // "magic link sent" confirmation. A visit without a pending magic link
6678 // forwards to /login — keeping the inlined form the single source of truth,
6779 // avoiding an orphaned duplicate page, and letting /login surface any flashed
68- // auth:error (e.g. an invalid-email submit that never sent a link) . The guard
80+ // auth:error. The guard
6981 // runs before reading auth:error so that error isn't consumed here before
7082 // /login can show it. An expired/invalid link click (routes/magic.tsx) is
7183 // different: the email-link strategy only clears the magic-link key on a
7284 // successful verify, so the key is still set and the request lands here — the
7385 // confirmation renders the flashed error as magicLinkError below.
7486 const url = new URL ( request . url ) ;
7587 const sanitized = sanitizeRedirectPath ( url . searchParams . get ( "redirectTo" ) ) ;
76- // The submitted address is carried on the success redirect so the
77- // confirmation can name it. Validate before echoing it back.
78- const emailParam = url . searchParams . get ( "email" ) ;
79- const email = emailParam && z . string ( ) . email ( ) . safeParse ( emailParam ) . success ? emailParam : null ;
88+ // The submitted address is carried in a short-lived cookie (not the URL) so
89+ // the confirmation can name it. Validate before echoing it back.
90+ const emailCookie = await magicLinkEmailCookie . parse ( request . headers . get ( "Cookie" ) ) ;
91+ const email =
92+ typeof emailCookie === "string" && z . string ( ) . email ( ) . safeParse ( emailCookie ) . success
93+ ? emailCookie
94+ : null ;
8095 if ( ! session . has ( "triggerdotdev:magiclink" ) ) {
8196 // Throw (not return) so the redirect doesn't widen the loader's return
8297 // type — otherwise useTypedLoaderData sees TypedResponse<never> in the
@@ -141,7 +156,7 @@ export async function action({ request }: ActionFunctionArgs) {
141156 message : "Please enter a valid email address." ,
142157 } ) ;
143158
144- return redirect ( "/login/magic " , {
159+ return redirect ( "/login" , {
145160 headers : {
146161 "Set-Cookie" : await commitSession ( session ) ,
147162 } ,
@@ -191,7 +206,7 @@ export async function action({ request }: ActionFunctionArgs) {
191206 message : errorMessage ,
192207 } ) ;
193208
194- return redirect ( "/login/magic " , {
209+ return redirect ( "/login" , {
195210 headers : {
196211 "Set-Cookie" : await commitSession ( session ) ,
197212 } ,
@@ -211,10 +226,20 @@ export async function action({ request }: ActionFunctionArgs) {
211226 return redirect ( ssoRedirect ) ;
212227 }
213228
214- return authenticator . authenticate ( "email-link" , request , {
215- successRedirect : `/login/magic?email=${ encodeURIComponent ( email ) } ` ,
216- failureRedirect : "/login/magic" ,
217- } ) ;
229+ // authenticator.authenticate throws its redirect Response; attach the
230+ // sent-to email as a short-lived cookie so the confirmation can name it
231+ // without putting the address in the URL.
232+ try {
233+ return await authenticator . authenticate ( "email-link" , request , {
234+ successRedirect : "/login/magic" ,
235+ failureRedirect : "/login" ,
236+ } ) ;
237+ } catch ( thrown ) {
238+ if ( thrown instanceof Response ) {
239+ thrown . headers . append ( "Set-Cookie" , await magicLinkEmailCookie . serialize ( email ) ) ;
240+ }
241+ throw thrown ;
242+ }
218243 }
219244 case "reset" :
220245 default : {
0 commit comments