Habiendo explorado cómo implementar un sistema de autenticación en Next.js 14 con NextAuth.js(Auth.js) en la primera parte de este blog, es crucial dar el siguiente paso para garantizar la validez de la información del usuario: la validación del correo electrónico.
Este proceso no es solo un paso adicional en la seguridad de nuestra aplicación, sino un componente esencial para garantizar que las interacciones entre el usuario y la plataforma sean legítimas y seguras.
En esta segunda parte, nos centraremos en integrar la validación de correo electrónico mediante el envío de correos electrónicos, utilizando Resend para enviar correos electrónicos y React Email para crear plantillas de correo electrónico atractivas y funcionales.
Asegúrate de que tu proyecto ya tenga implementado el sistema de autenticación descrito en la primera parte del blog. Esto incluye tener Next.js 14 y NextAuth configurados correctamente.
Instale las dependencias necesarias en el proyecto. Esta vez usaremos pnpm
Puedes usar el administrador de paquetes de tu elección.
pnpm add resend react-email @react-email/components
2. Cree la siguiente estructura para el proyecto:
... ├── emails/ │ └── verification-template.tsx ... ├── src/ │ ├── actions/ │ │ ├── email-actions.tsx │ │ └── auth-actions.tsx │ ├── app/ │ │ ... │ │ ├── (primary)/ │ │ │ ├── auth/ │ │ │ │ └── verify-email/ │ │ │ │ └── page.tsx │ │ │ ├── layout.tsx │ │ │ └── page.tsx │ │ │ ... │ ├── components/ │ │ └── auth/ │ │ ├── signin-form.tsx │ │ ├── signup-form.tsx │ │ ... │ ... │ ├── utils.ts │ ... ... ├── .env ...
React Email te permite crear plantillas de correo electrónico usando JSX, lo que facilita la creación de correos electrónicos atractivos y consistentes con tu marca.
Creemos una plantilla de correo electrónico básica como componente de React. En este caso crearemos la plantilla que se enviará para que el usuario confirme su correo electrónico.
emails/verification-template.tsx
:
// Import React and necessary components from @react-email/components import * as React from 'react'; import { Body, Button, Container, Head, Hr, Html, Img, Preview, Section, Text } from '@react-email/components'; import { getBaseUrl } from '@/utils'; // Obtain the base URL using the imported function const baseUrl = getBaseUrl(); // Define the properties expected by the VerificationTemplate component interface VerificationTemplateProps { username: string; emailVerificationToken: string; } // Define the VerificationTemplate component that takes the defined properties export const VerificationTemplate = ({ username, emailVerificationToken }: VerificationTemplateProps) => ( <Html> <Head /> <Preview>Preview text that appears in the email client before opening the email.</Preview> <Body style={main}> <Container style={container}> <Img src='my-logo.png' alt='My SaaS' style={logo} /> <Text style={title}>Hi {username}!</Text> <Text style={title}>Welcome to Starter Kit for building a SaaS</Text> <Text style={paragraph}>Please verify your email, with the link below:</Text> <Section style={btnContainer}> {/* Button that takes the user to the verification link */} <Button style={button} href={`${baseUrl}/auth/verify-email?token=${emailVerificationToken}`} > Click here to verify </Button> </Section> <Hr style={hr} /> <Text style={footer}>Something in the footer.</Text> </Container> </Body> </Html> ); // Styles applied to different parts of the email for customization const main = { backgroundColor: '#020817', color: '#ffffff', fontFamily: '-apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Oxygen-Sans, Ubuntu, Cantarell, "Helvetica Neue", sans-serif', }; const container = { margin: '0 auto', padding: '20px 0 48px', }; ...
Este componente crea una plantilla de correo electrónico HTML que incluye estilos y contenido dinámico.
Se definen propiedades para recibir username
y emailVerificationToken
. Estas propiedades se utilizan para personalizar el correo electrónico del usuario y generar el enlace de verificación.
Para validar y probar la plantilla, React Email proporciona un comando para ejecutar localmente un servidor que expondrá las plantillas que hemos creado dentro de la carpeta de correos electrónicos.
Creamos el script en el package.json
para ejecutar el servidor.
{ "scripts": { "dev": "email dev" } }
2. Ejecutamos el script y esto ejecutará el servidor en localhost
; veremos una pantalla como la siguiente con todas las plantillas creadas.
En nuestro caso, tenemos una sola plantilla. Como puede ver a continuación, tenemos una vista previa del correo electrónico que se enviará al usuario.
API KEY
al archivo .env
.
... # resend RESEND_API_KEY="re_jYiFaXXXXXXXXXXXXXXXXXXX"
4. Para crear la funcionalidad de envío de correos electrónicos, podríamos crear endpoints dentro de la carpeta api/
y realizar solicitudes http
, sin embargo, en esta ocasión lo haremos aprovechando el potencial de acciones del servidor.
actions/email-actions.ts
:
'use server' import React from 'react' import { Resend } from 'resend' // Creates an instance of Resend using the API KEY const resend = new Resend(process.env.RESEND_API_KEY) // Defines the data structure for an email. interface Email { to: string[] // An array of email addresses to which to send the email. subject: string // The subject of the email. react: React.ReactElement // The body of the email as a React element. } export const sendEmail = async (payload: Email) => { const { error } = await resend.emails.send({ from: 'My SaaS <[email protected]>', // Defines the sender's address. ...payload, // Expands the contents of 'payload' to include 'to', 'subject', and 'react'. }) if (error) { console.error('Error sending email', error) return null } console.log('Email sent successfully') return true }
Nota: Para probar en desarrollo libre, debes utilizar como remitente el correo electrónico “[email protected]”, de lo contrario, tendrás que agregar un dominio personalizado.
5. Enviar el correo electrónico al registrar un nuevo usuario.
actions/auth-actions.ts
:
... import { sendEmail } from './email-actions' import VerificationTemplate from '../../emails/verification-template' // Import a utility function to generate a secure token. import { generateSecureToken } from '@/utils' export async function registerUser(user: Partial<User>) { try { // Creates a new user in the database with the provided data. // Passwords are hashed using bcrypt for security. const createdUser = await prisma.user.create({ data: { ...user, password: await bcrypt.hash(user.password as string, 10), } as User, }) // Generates a secure token to be used for email verification. const emailVerificationToken = generateSecureToken() // Updates the newly created user with the email verification token. await prisma.user.update({ where: { id: createdUser.id, }, data: { emailVerificationToken, }, }) // Sends a verification email to the new user using the sendEmail function. await sendEmail({ to: ['your Resend registered email', createdUser.email], subject: 'Verify your email address', react: React.createElement(VerificationTemplate, { username: createdUser.username, emailVerificationToken }), }) return createdUser } catch (error) { console.log(error) if (error instanceof Prisma.PrismaClientKnownRequestError) { if (error.code === 'P2002') { // Returns a custom error message if the email already exists in the database. return { error: 'Email already exists.' } } } return { error: 'An unexpected error occurred.' } } }
Una vez creado el usuario y generado el token de verificación, la función envía un correo electrónico al nuevo usuario.
Este correo electrónico se construye utilizando el componente VerificationTemplate
React, personalizado con el nombre del usuario y el token de verificación. Este paso es crucial para verificar que la dirección de correo electrónico del usuario sea válida y esté controlada por el usuario.
Una vez enviado el correo electrónico al usuario, este tendrá un enlace que lo llevará de regreso al sitio. Para validar el correo electrónico, para ello necesitamos crear la página.
(primary)/auth/verify-email/page.tsx
:
/* All imports */ // Defines the prop types for the VerifyEmailPage component. interface VerifyEmailPageProps { searchParams: { [key: string]: string | string[] | undefined } } export default async function VerifyEmailPage({ searchParams }: VerifyEmailPageProps) { let message = 'Verifying email...' let verified = false if (searchParams.token) { // Checks if a verification token is provided in the URL. // Attempts to find a user in the database with the provided email verification token. const user = await prisma.user.findUnique({ where: { emailVerificationToken: searchParams.token as string, }, }) // Conditionally updates the message and verified status based on the user lookup. if (!user) { message = 'User not found. Check your email for the verification link.' } else { // If the user is found, updates the user record to mark the email as verified. await prisma.user.update({ where: { emailVerificationToken: searchParams.token as string, }, data: { emailVerified: true, emailVerificationToken: null, // Clears the verification token. }, }) message = `Email verified! ${user.email}` verified = true // Sets the verified status to true. } } else { // Updates the message if no verification token is found. message = 'No email verification token found. Check your email.' } return ( <div className='grid place-content-center py-40'> <Card className='max-w-sm text-center'> <CardHeader> <CardTitle>Email Verification</CardTitle> </CardHeader> <CardContent> <div className='w-full grid place-content-center py-4'> {verified ? <EmailCheckIcon size={56} /> : <EmailWarningIcon size={56} />} </div> <p className='text-lg text-muted-foreground' style={{ textWrap: 'balance' }}> {message} </p> </CardContent> <CardFooter> {verified && ( // Displays a sign-in link if the email is successfully verified. <Link href={'/auth/signin'} className='bg-primary text-white text-sm font-medium hover:bg-primary/90 h-10 px-4 py-2 rounded-lg w-full text-center'> Sign in </Link> )} </CardFooter> </Card> </div> ) }
Luego de validar exitosamente el correo electrónico del usuario, veremos el siguiente mensaje.
Ahora, implementaremos una última validación para cuando el usuario quiera iniciar sesión y aún no haya verificado su correo electrónico.
components/auth/sigin-form.tsx
:
... async function onSubmit(values: InputType) { try { setIsLoading(true) const response = await signIn('credentials', { redirect: false, email: values.email, password: values.password, }) if (!response?.ok) { // if the email is not verified we will show a message to the user. if (response?.error === 'EmailNotVerified') { toast({ title: 'Please, verify your email first.', variant: 'warning', }) return } toast({ title: 'Something went wrong!', description: response?.error, variant: 'destructive', }) return } toast({ title: 'Welcome back! ', description: 'Redirecting you to your dashboard!', }) router.push(callbackUrl ? callbackUrl : '/') } catch (error) { console.log({ error }) toast({ title: 'Something went wrong!', description: "We couldn't create your account. Please try again later!", variant: 'destructive', }) } finally { setIsLoading(false) } } ...
¡Eso es todo! 🎉
El usuario podrá validar su correo electrónico y finalizar su registro en nuestra aplicación.
🧑💻
Ya sabemos cómo crear y enviar correos electrónicos usando React Email y Resend. Este proceso le permite aprovechar su conocimiento de React para diseñar correos electrónicos de manera eficiente mientras mantiene un flujo de trabajo familiar y productivo.
Puede experimentar con diferentes componentes y propiedades para crear correos electrónicos que se adapten perfectamente a las necesidades de sus proyectos.
¿Quieres conectarte con el autor?
Me encanta conectarme con amigos de todo el mundo en 𝕏 .
También publicado aquí