-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
Showing
12 changed files
with
1,494 additions
and
11 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,48 @@ | ||
import { | ||
Html, | ||
Head, | ||
Preview, | ||
Body, | ||
Container, | ||
Heading, | ||
Link, | ||
Text, | ||
} from '@react-email/components' | ||
import { type User } from '#drizzle/schema.ts' | ||
|
||
export namespace MagicLink { | ||
export type Props = { | ||
magicLink: string | ||
user?: User | ||
} | ||
} | ||
export function MagicLinkEmail({ magicLink, user }: MagicLink.Props) { | ||
return ( | ||
<Html> | ||
<Head /> | ||
<Preview>Log in or sign up with your magic link.</Preview> | ||
<Body> | ||
<Container> | ||
<Heading as="h2"> | ||
{user | ||
? `Hey ${user.display_name}! Welcome back!` | ||
: `Hey there! Welcome!`} | ||
</Heading> | ||
|
||
<Heading as="h3">🎩 Here's your magic link</Heading> | ||
<Link href={magicLink}>👉 {user ? 'Log In' : 'Sign Up'}</Link> | ||
<Text> | ||
If you didn’t request this email, you can safely ignore it. | ||
</Text> | ||
<Text> | ||
This link is only valid for ten minutes. | ||
<br /> | ||
If it has expired, you can request a new one. | ||
</Text> | ||
</Container> | ||
</Body> | ||
</Html> | ||
) | ||
} | ||
|
||
export default MagicLinkEmail |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,29 @@ | ||
import * as React from "react" | ||
import * as SeparatorPrimitive from "@radix-ui/react-separator" | ||
|
||
import { cn } from "#app/utils/ui.ts" | ||
|
||
const Separator = React.forwardRef< | ||
React.ComponentRef<typeof SeparatorPrimitive.Root>, | ||
React.ComponentPropsWithoutRef<typeof SeparatorPrimitive.Root> | ||
>( | ||
( | ||
{ className, orientation = "horizontal", decorative = true, ...props }, | ||
ref | ||
) => ( | ||
<SeparatorPrimitive.Root | ||
ref={ref} | ||
decorative={decorative} | ||
orientation={orientation} | ||
className={cn( | ||
"shrink-0 bg-border", | ||
orientation === "horizontal" ? "h-[1px] w-full" : "h-full w-[1px]", | ||
className | ||
)} | ||
{...props} | ||
/> | ||
) | ||
) | ||
Separator.displayName = SeparatorPrimitive.Root.displayName | ||
|
||
export { Separator } |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,92 @@ | ||
import { redirect } from 'react-router' | ||
import { getUserId, login } from '#app/utils/auth/auth.server.ts' | ||
import { createAuthenticator } from '#app/utils/auth/magic-link.server.ts' | ||
import { onboardingCookie } from '#app/utils/auth/onboarding.server.ts' | ||
import { db } from '#app/utils/db.server.ts' | ||
import { getRedirect } from '#app/utils/redirect.server.ts' | ||
import { mergeHeaders } from '#app/utils/request.server.ts' | ||
import { ServerTiming } from '#app/utils/timings.server.ts' | ||
import { redirectWithToast } from '#app/utils/toast.server.ts' | ||
import { type Route } from './+types/auth.magic' | ||
|
||
export const loader = async ({ request }: Route.LoaderArgs) => { | ||
const authenticator = createAuthenticator(request) | ||
const timing = new ServerTiming() | ||
|
||
timing.time('authenticate email', 'Authenticate email') | ||
const authResult = await authenticator | ||
.authenticate('email-link', request) | ||
.then( | ||
(data) => | ||
({ | ||
success: true, | ||
data, | ||
}) as const, | ||
(error) => | ||
({ | ||
success: false, | ||
error, | ||
}) as const, | ||
) | ||
timing.timeEnd('authenticate email') | ||
|
||
if (!authResult.success) { | ||
console.error(authResult.error) | ||
|
||
throw redirect('/login') | ||
} | ||
|
||
const email = authResult.data | ||
|
||
const { redirectTo, discardHeaders } = getRedirect(request) ?? {} | ||
const headers = new Headers() | ||
mergeHeaders(headers, discardHeaders) | ||
|
||
timing.time('get user id', 'Get user id in database') | ||
const userId = await getUserId(request) | ||
timing.timeEnd('get user id') | ||
|
||
// logged in | ||
if (userId) { | ||
headers.append('Server-Timing', timing.toString()) | ||
return redirectWithToast( | ||
'/settings/profile', | ||
{ | ||
type: 'error', | ||
title: 'Already Signed In', | ||
message: `You are already signed in with an account.`, | ||
}, | ||
{ headers }, | ||
) | ||
} | ||
|
||
timing.time('get email owner', 'Get email owner in database') | ||
const emailOwner = await db.query.userTable.findFirst({ | ||
where: (userTable, { eq }) => eq(userTable.email, email), | ||
}) | ||
timing.timeEnd('get email owner') | ||
|
||
if (emailOwner) { | ||
headers.append('Server-Timing', timing.toString()) | ||
return await login( | ||
{ | ||
request, | ||
redirectTo, | ||
userId: emailOwner.id, | ||
}, | ||
{ headers }, | ||
) | ||
} | ||
|
||
// real new user, send to onboarding | ||
headers.append( | ||
'set-cookie', | ||
await onboardingCookie.serialize({ | ||
type: 'magic-link', | ||
email, | ||
}), | ||
) | ||
|
||
headers.append('Server-Timing', timing.toString()) | ||
throw redirect('/onboarding', { headers }) | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,24 @@ | ||
import { EmailLinkStrategy } from '@nichtsam/remix-auth-email-link' | ||
import { Authenticator } from 'remix-auth' | ||
import { sendMagicLinkEmail } from '../email.server' | ||
import { env } from '../env.server' | ||
import { getDomainUrl } from '../request.server' | ||
|
||
export const createAuthenticator = (request: Request) => { | ||
const authenticator = new Authenticator<string>() | ||
const magicEndpoint = new URL('/auth/magic', getDomainUrl(request)) | ||
authenticator.use( | ||
new EmailLinkStrategy( | ||
{ | ||
secret: env.MAGIC_LINK_SECRET, | ||
shouldValidateSessionMagicLink: true, | ||
magicEndpoint, | ||
sendEmail: ({ email, magicLink }) => | ||
sendMagicLinkEmail({ email, magicLink, domain: magicEndpoint.host }), | ||
}, | ||
async ({ email }) => email, | ||
), | ||
) | ||
|
||
return authenticator | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,34 @@ | ||
import { render } from '@react-email/components' | ||
import { Resend } from 'resend' | ||
import MagicLinkEmail from '#app/components/emails/magic-link.tsx' | ||
import { db } from './db.server' | ||
import { env } from './env.server' | ||
|
||
const resend = new Resend(env.RESEND_API_KEY) | ||
|
||
export async function sendMagicLinkEmail({ | ||
email, | ||
magicLink, | ||
}: { | ||
email: string | ||
magicLink: string | ||
domain: string | ||
}) { | ||
const subject = `🎩 Here's your magic link for nichtsam.com` | ||
const from = 'nichtsam <[email protected]>' | ||
const to = email | ||
|
||
const user = await db.query.userTable.findFirst({ | ||
where: (record, { eq }) => eq(record.email, email), | ||
}) | ||
|
||
const react = <MagicLinkEmail magicLink={magicLink} user={user} /> | ||
|
||
await resend.emails.send({ | ||
subject, | ||
from, | ||
to, | ||
react, | ||
text: await render(react, { plainText: true }), | ||
}) | ||
} |
Oops, something went wrong.