feat(email): refactor email module and improve config handling

- Simplify `sendEmail` by extracting `resolveFrom` and `buildPayload` helpers
- Remove `sendAuthEmail` and `sendAppEmail` exports, keeping only `sendEmail` and `sendBatchEmails`
- Replace hardcoded fallback sender with env-based validation (throws if missing)
- Update `BaseLayout` to resolve `supportEmail` from `ZEN_SUPPORT_EMAIL` env var instead of hardcoded default
- Conditionally render support section only when a support email is available
- Remove verbose JSDoc comments and reduce overall code verbosity
This commit is contained in:
2026-04-13 18:37:06 -04:00
parent 59fce3cd91
commit 87a04db04b
10 changed files with 166 additions and 518 deletions
+30 -176
View File
@@ -1,211 +1,65 @@
/**
* Email Utility using Resend
* Centralized email sending functionality for the entire package
*/
import { Resend } from 'resend';
import { done, fail, info } from '../../shared/lib/logger.js';
/**
* Initialize Resend client
*/
let resendClient = null;
function getResendClient() {
if (!resendClient) {
const apiKey = process.env.ZEN_EMAIL_RESEND_APIKEY;
if (!apiKey) {
throw new Error('ZEN_EMAIL_RESEND_APIKEY environment variable is not set');
}
if (!apiKey) throw new Error('ZEN_EMAIL_RESEND_APIKEY environment variable is not set');
resendClient = new Resend(apiKey);
}
return resendClient;
}
/**
* Format sender address with name if available
* @param {string} email - Email address
* @param {string} name - Sender name (optional)
* @returns {string} Formatted sender address
*/
function formatSenderAddress(email, name) {
if (name && name.trim()) {
return `${name.trim()} <${email}>`;
}
return email;
function resolveFrom(from, fromName) {
const address = from || process.env.ZEN_EMAIL_FROM_ADDRESS;
if (!address) throw new Error('ZEN_EMAIL_FROM_ADDRESS environment variable is not set');
const name = fromName || process.env.ZEN_EMAIL_FROM_NAME;
return name?.trim() ? `${name.trim()} <${address}>` : address;
}
/**
* Send an email using Resend
* @param {Object} options - Email options
* @param {string} options.to - Recipient email address
* @param {string} options.subject - Email subject
* @param {string} options.html - HTML content of the email
* @param {string} options.text - Plain text content of the email (optional)
* @param {string} options.from - Sender email address (optional, defaults to ZEN_EMAIL_FROM_ADDRESS)
* @param {string} options.fromName - Sender name (optional, defaults to ZEN_EMAIL_FROM_NAME)
* @param {string} options.replyTo - Reply-to email address (optional)
* @param {Array} options.attachments - Email attachments (optional)
* @param {Object} options.tags - Email tags for tracking (optional)
* @returns {Promise<Object>} Resend response
*/
async function sendEmail({ to, subject, html, text, from, fromName, replyTo, attachments, tags }) {
function buildPayload({ to, subject, html, text, from, fromName, replyTo, attachments, tags }) {
return {
from: resolveFrom(from, fromName),
to,
subject,
html,
...(text && { text }),
...(replyTo && { reply_to: replyTo }),
...(attachments && { attachments }),
...(tags && { tags })
};
}
async function sendEmail(email) {
try {
const resend = getResendClient();
// Default from address and name
const fromAddress = from || process.env.ZEN_EMAIL_FROM_ADDRESS || 'noreply@example.com';
const senderName = fromName || process.env.ZEN_EMAIL_FROM_NAME;
// Format sender with name if available
const formattedFrom = formatSenderAddress(fromAddress, senderName);
const emailData = {
from: formattedFrom,
to,
subject,
html,
...(text && { text }),
...(replyTo && { reply_to: replyTo }),
...(attachments && { attachments }),
...(tags && { tags })
};
const response = await resend.emails.send(emailData);
// Resend returns { data: { id: "..." }, error: null } or { data: null, error: { message: "..." } }
const response = await getResendClient().emails.send(buildPayload(email));
if (response.error) {
fail(`Email Resend error: ${response.error.message || response.error}`);
return {
success: false,
data: null,
error: response.error.message || 'Failed to send email'
};
return { success: false, data: null, error: response.error.message || 'Failed to send email' };
}
const emailId = response.data?.id || response.id;
info(`Email sent to ${to} — ID: ${emailId}`);
return {
success: true,
data: response.data || response,
error: null
};
info(`Email sent to ${email.to} — ID: ${response.data?.id}`);
return { success: true, data: response.data, error: null };
} catch (error) {
fail(`Email send failed: ${error.message}`);
return {
success: false,
data: null,
error: error.message
};
return { success: false, data: null, error: error.message };
}
}
/**
* Send an authentication-related email
* Uses default ZEN_EMAIL_FROM_ADDRESS and ZEN_EMAIL_FROM_NAME
* @param {Object} options - Email options
* @param {string} options.to - Recipient email address
* @param {string} options.subject - Email subject
* @param {string} options.html - HTML content of the email
* @param {string} options.text - Plain text content of the email (optional)
* @param {string} options.replyTo - Reply-to email address (optional)
* @returns {Promise<Object>} Resend response
*/
async function sendAuthEmail({ to, subject, html, text, replyTo }) {
return sendEmail({
to,
subject,
html,
text,
replyTo
});
}
/**
* Send an application-related email
* Uses default ZEN_EMAIL_FROM_ADDRESS and ZEN_EMAIL_FROM_NAME
* @param {Object} options - Email options
* @param {string} options.to - Recipient email address
* @param {string} options.subject - Email subject
* @param {string} options.html - HTML content of the email
* @param {string} options.text - Plain text content of the email (optional)
* @param {string} options.replyTo - Reply-to email address (optional)
* @param {Array} options.attachments - Email attachments (optional)
* @param {Object} options.tags - Email tags for tracking (optional)
* @returns {Promise<Object>} Resend response
*/
async function sendAppEmail({ to, subject, html, text, replyTo, attachments, tags }) {
return sendEmail({
to,
subject,
html,
text,
replyTo,
attachments,
tags
});
}
/**
* Send a batch of emails
* @param {Array<Object>} emails - Array of email objects
* @returns {Promise<Array<Object>>} Array of Resend responses
*/
async function sendBatchEmails(emails) {
try {
const resend = getResendClient();
const emailsData = emails.map(email => {
const fromAddress = email.from || process.env.ZEN_EMAIL_FROM_ADDRESS || 'noreply@example.com';
const fromName = email.fromName || process.env.ZEN_EMAIL_FROM_NAME;
const formattedFrom = formatSenderAddress(fromAddress, fromName);
return {
from: formattedFrom,
to: email.to,
subject: email.subject,
html: email.html,
...(email.text && { text: email.text }),
...(email.replyTo && { reply_to: email.replyTo }),
...(email.attachments && { attachments: email.attachments }),
...(email.tags && { tags: email.tags })
};
});
const response = await resend.batch.send(emailsData);
// Handle Resend error response
const response = await getResendClient().batch.send(emails.map(buildPayload));
if (response.error) {
fail(`Email batch Resend error: ${response.error.message || response.error}`);
return {
success: false,
data: null,
error: response.error.message || 'Failed to send batch emails'
};
return { success: false, data: null, error: response.error.message || 'Failed to send batch emails' };
}
done(`Email batch of ${emails.length} sent`);
return {
success: true,
data: response.data || response,
error: null
};
return { success: true, data: response.data, error: null };
} catch (error) {
fail(`Email batch send failed: ${error.message}`);
return {
success: false,
data: null,
error: error.message
};
return { success: false, data: null, error: error.message };
}
}
export {
sendEmail,
sendAuthEmail,
sendAppEmail,
sendBatchEmails
};
export { sendEmail, sendBatchEmails };
+9 -8
View File
@@ -18,18 +18,19 @@ import {
Link,
} from "@react-email/components";
export const BaseLayout = ({
preview,
title,
children,
export const BaseLayout = ({
preview,
title,
children,
companyName,
logoURL,
supportSection = false,
supportEmail = 'support@zenya.test'
supportEmail
}) => {
const appName = companyName || process.env.ZEN_NAME || 'ZEN';
const logoSrc = logoURL || process.env.ZEN_EMAIL_LOGO || null;
const logoHref = process.env.ZEN_EMAIL_LOGO_URL || null;
const resolvedSupportEmail = supportEmail || process.env.ZEN_SUPPORT_EMAIL;
const currentYear = new Date().getFullYear();
return (
@@ -68,11 +69,11 @@ export const BaseLayout = ({
<Text className="text-[12px] leading-[20px] text-neutral-400 m-0">
© {currentYear} {appName}. Tous droits réservés.
{supportSection && (
{supportSection && resolvedSupportEmail && (
<>
{' · '}
<Link href={`mailto:${supportEmail}`} className="text-neutral-400 underline">
{supportEmail}
<Link href={`mailto:${resolvedSupportEmail}`} className="text-neutral-400 underline">
{resolvedSupportEmail}
</Link>
</>
)}
@@ -1,41 +0,0 @@
/**
* Password Changed Confirmation Email Template
*/
import { Section, Text } from "@react-email/components";
import { BaseLayout } from "./BaseLayout";
export const PasswordChangedEmail = ({
email,
companyName
}) => {
const appName = companyName || process.env.ZEN_NAME || 'ZEN';
const supportEmail = process.env.ZEN_SUPPORT_EMAIL || 'support@zenya.test';
return (
<BaseLayout
preview={`Votre mot de passe a été modifié ${appName}`}
title="Mot de passe modifié"
companyName={companyName}
supportSection={true}
supportEmail={supportEmail}
>
<Text className="text-[14px] leading-[24px] text-neutral-600 mt-[4px] mb-[24px]">
Ceci confirme que le mot de passe de votre compte <span className="font-medium text-neutral-900">{appName}</span> a bien été modifié.
</Text>
<Section style={{ border: '1px solid #E5E5E5' }} className="bg-neutral-100 rounded-[12px] p-[20px] my-[24px]">
<Text className="text-[12px] font-medium text-neutral-400 m-0 mb-[4px] uppercase tracking-wider">
Compte
</Text>
<Text className="text-[14px] font-medium text-neutral-900 m-0">
{email}
</Text>
</Section>
<Text className="text-[12px] leading-[20px] text-neutral-400 m-0">
Si vous n'êtes pas à l'origine de cette modification, contactez immédiatement notre équipe de support.
</Text>
</BaseLayout>
);
};
@@ -1,49 +0,0 @@
/**
* Password Reset Email Template
*/
import { Button, Section, Text, Link } from "@react-email/components";
import { BaseLayout } from "./BaseLayout";
export const PasswordResetEmail = ({
email,
resetUrl,
companyName
}) => {
const appName = companyName || process.env.ZEN_NAME || 'ZEN';
const supportEmail = process.env.ZEN_SUPPORT_EMAIL || 'support@zenya.test';
return (
<BaseLayout
preview={`Réinitialisez votre mot de passe pour ${appName}`}
title="Réinitialisation du mot de passe"
companyName={companyName}
supportSection={true}
supportEmail={supportEmail}
>
<Text className="text-[14px] leading-[24px] text-neutral-600 mt-[4px] mb-[24px]">
Nous avons reçu une demande de réinitialisation du mot de passe pour votre compte <span className="font-medium text-neutral-900">{appName}</span>. Cliquez sur le bouton ci-dessous pour en choisir un nouveau.
</Text>
<Section className="mt-[28px] mb-[32px]">
<Button
href={resetUrl}
className="bg-neutral-900 rounded-[8px] text-white font-medium px-[20px] py-[11px] no-underline text-[13px]"
>
Réinitialiser le mot de passe
</Button>
</Section>
<Text className="text-[12px] leading-[20px] text-neutral-400 m-0">
Ce lien expire dans 1 heure. Si vous n'êtes pas à l'origine de cette demande, ignorez ce message votre mot de passe ne sera pas modifié.
</Text>
<Text className="text-[12px] leading-[20px] text-neutral-400 m-0 mt-[8px]">
Lien :{' '}
<Link href={resetUrl} className="text-neutral-400 underline break-all">
{resetUrl}
</Link>
</Text>
</BaseLayout>
);
};
@@ -1,49 +0,0 @@
/**
* Email Verification Template
*/
import { Button, Section, Text, Link } from "@react-email/components";
import { BaseLayout } from "./BaseLayout";
export const VerificationEmail = ({
email,
verificationUrl,
companyName
}) => {
const appName = companyName || process.env.ZEN_NAME || 'ZEN';
const supportEmail = process.env.ZEN_SUPPORT_EMAIL || 'support@zenya.test';
return (
<BaseLayout
preview={`Confirmez votre adresse courriel pour ${appName}`}
title="Confirmez votre adresse courriel"
companyName={companyName}
supportSection={true}
supportEmail={supportEmail}
>
<Text className="text-[14px] leading-[24px] text-neutral-600 mt-[4px] mb-[24px]">
Merci de vous être inscrit sur <span className="font-medium text-neutral-900">{appName}</span>. Cliquez sur le bouton ci-dessous pour confirmer votre adresse courriel.
</Text>
<Section className="mt-[28px] mb-[32px]">
<Button
href={verificationUrl}
className="bg-neutral-900 rounded-[8px] text-white font-medium px-[20px] py-[11px] no-underline text-[13px]"
>
Confirmer mon courriel
</Button>
</Section>
<Text className="text-[12px] leading-[20px] text-neutral-400 m-0">
Ce lien expire dans 24 heures. Si vous n'avez pas créé de compte, ignorez ce message.
</Text>
<Text className="text-[12px] leading-[20px] text-neutral-400 m-0 mt-[8px]">
Lien :{' '}
<Link href={verificationUrl} className="text-neutral-400 underline break-all">
{verificationUrl}
</Link>
</Text>
</BaseLayout>
);
};
-70
View File
@@ -1,71 +1 @@
/**
* Email Templates
* Export all email templates and render functions
*/
import { render } from '@react-email/components';
import { VerificationEmail } from './VerificationEmail.jsx';
import { PasswordResetEmail } from './PasswordResetEmail.jsx';
import { PasswordChangedEmail } from './PasswordChangedEmail.jsx';
// Export JSX components
export { VerificationEmail } from './VerificationEmail.jsx';
export { PasswordResetEmail } from './PasswordResetEmail.jsx';
export { PasswordChangedEmail } from './PasswordChangedEmail.jsx';
export { BaseLayout } from './BaseLayout.jsx';
/**
* Render verification email to HTML
* @param {string} verificationUrl - The verification URL
* @param {string} email - User's email address
* @param {string} companyName - Company name (optional)
* @returns {Promise<string>} Rendered HTML
*/
export async function renderVerificationEmail(verificationUrl, email, companyName) {
return await render(
<VerificationEmail
email={email}
verificationUrl={verificationUrl}
companyName={companyName}
/>
);
}
/**
* Render password reset email to HTML
* @param {string} resetUrl - The password reset URL
* @param {string} email - User's email address
* @param {string} companyName - Company name (optional)
* @returns {Promise<string>} Rendered HTML
*/
export async function renderPasswordResetEmail(resetUrl, email, companyName) {
return await render(
<PasswordResetEmail
email={email}
resetUrl={resetUrl}
companyName={companyName}
/>
);
}
/**
* Render password changed email to HTML
* @param {string} email - User's email address
* @param {string} companyName - Company name (optional)
* @returns {Promise<string>} Rendered HTML
*/
export async function renderPasswordChangedEmail(email, companyName) {
return await render(
<PasswordChangedEmail
email={email}
companyName={companyName}
/>
);
}
// Legacy exports for backward compatibility
export const getVerificationEmailTemplate = renderVerificationEmail;
export const getPasswordResetTemplate = renderPasswordResetEmail;
export const getPasswordChangedTemplate = renderPasswordChangedEmail;
+29 -125
View File
@@ -1,61 +1,38 @@
/**
* Email Verification and Password Reset
* Handles email verification tokens and password reset tokens
*/
import crypto from 'crypto';
import { render } from '@react-email/components';
import { create, findOne, deleteWhere } from '../../../core/database/crud.js';
import { generateToken, generateId } from './password.js';
import { fail, info } from '../../../shared/lib/logger.js';
import { sendAuthEmail } from '../../../core/email/index.js';
import { renderVerificationEmail, renderPasswordResetEmail, renderPasswordChangedEmail } from '../../../core/email/templates/index.js';
import { sendEmail } from '../../../core/email/index.js';
import { VerificationEmail } from '../templates/VerificationEmail.jsx';
import { PasswordResetEmail } from '../templates/PasswordResetEmail.jsx';
import { PasswordChangedEmail } from '../templates/PasswordChangedEmail.jsx';
/**
* Create an email verification token
* @param {string} email - User email
* @returns {Promise<Object>} Verification object with token
*/
async function createEmailVerification(email) {
const token = generateToken(32);
const verificationId = generateId();
// Token expires in 24 hours
const expiresAt = new Date();
expiresAt.setHours(expiresAt.getHours() + 24);
// Delete any existing verification tokens for this email
await deleteWhere('zen_auth_verifications', {
identifier: 'email_verification',
value: email
});
await deleteWhere('zen_auth_verifications', { identifier: 'email_verification', value: email });
const verification = await create('zen_auth_verifications', {
id: verificationId,
id: generateId(),
identifier: 'email_verification',
value: email,
token,
expires_at: expiresAt,
updated_at: new Date()
});
return {
...verification,
token
};
return { ...verification, token };
}
/**
* Verify an email token
* @param {string} email - User email
* @param {string} token - Verification token
* @returns {Promise<boolean>} True if valid, false otherwise
*/
async function verifyEmailToken(email, token) {
const verification = await findOne('zen_auth_verifications', {
identifier: 'email_verification',
value: email
});
if (!verification) return false;
// Timing-safe comparison — always operate on same-length buffers so that a
@@ -69,64 +46,40 @@ async function verifyEmailToken(email, token) {
&& token.length === verification.token.length;
if (!tokensMatch) return false;
// Check if token is expired
if (new Date(verification.expires_at) < new Date()) {
await deleteWhere('zen_auth_verifications', { id: verification.id });
return false;
}
// Delete the verification token after use
await deleteWhere('zen_auth_verifications', { id: verification.id });
return true;
}
/**
* Create a password reset token
* @param {string} email - User email
* @returns {Promise<Object>} Reset object with token
*/
async function createPasswordReset(email) {
const token = generateToken(32);
const resetId = generateId();
// Token expires in 1 hour
const expiresAt = new Date();
expiresAt.setHours(expiresAt.getHours() + 1);
// Delete any existing reset tokens for this email
await deleteWhere('zen_auth_verifications', {
identifier: 'password_reset',
value: email
});
await deleteWhere('zen_auth_verifications', { identifier: 'password_reset', value: email });
const reset = await create('zen_auth_verifications', {
id: resetId,
id: generateId(),
identifier: 'password_reset',
value: email,
token,
expires_at: expiresAt,
updated_at: new Date()
});
return {
...reset,
token
};
return { ...reset, token };
}
/**
* Verify a password reset token
* @param {string} email - User email
* @param {string} token - Reset token
* @returns {Promise<boolean>} True if valid, false otherwise
*/
async function verifyResetToken(email, token) {
const reset = await findOne('zen_auth_verifications', {
identifier: 'password_reset',
value: email
});
if (!reset) return false;
// Timing-safe comparison — same rationale as verifyEmailToken above.
@@ -139,7 +92,6 @@ async function verifyResetToken(email, token) {
&& token.length === reset.token.length;
if (!tokensMatch) return false;
// Check if token is expired
if (new Date(reset.expires_at) < new Date()) {
await deleteWhere('zen_auth_verifications', { id: reset.id });
return false;
@@ -148,92 +100,44 @@ async function verifyResetToken(email, token) {
return true;
}
/**
* Delete a password reset token
* @param {string} email - User email
* @returns {Promise<number>} Number of deleted tokens
*/
async function deleteResetToken(email) {
return await deleteWhere('zen_auth_verifications', {
identifier: 'password_reset',
value: email
});
function deleteResetToken(email) {
return deleteWhere('zen_auth_verifications', { identifier: 'password_reset', value: email });
}
/**
* Send verification email using Resend
* @param {string} email - User email
* @param {string} token - Verification token
* @param {string} baseUrl - Base URL of the application
*/
async function sendVerificationEmail(email, token, baseUrl) {
const verificationUrl = `${baseUrl}/auth/confirm?email=${encodeURIComponent(email)}&token=${token}`;
const appName = process.env.ZEN_NAME || 'ZEN';
const html = await renderVerificationEmail(verificationUrl, email, appName);
const result = await sendAuthEmail({
to: email,
subject: `Confirmez votre adresse courriel ${appName}`,
html
});
const verificationUrl = `${baseUrl}/auth/confirm?email=${encodeURIComponent(email)}&token=${token}`;
const html = await render(<VerificationEmail verificationUrl={verificationUrl} companyName={appName} />);
const result = await sendEmail({ to: email, subject: `Confirmez votre adresse courriel ${appName}`, html });
if (!result.success) {
fail(`Auth: failed to send verification email to ${email}: ${result.error}`);
throw new Error('Failed to send verification email');
}
info(`Auth: verification email sent to ${email}`);
return result;
}
/**
* Send password reset email using Resend
* @param {string} email - User email
* @param {string} token - Reset token
* @param {string} baseUrl - Base URL of the application
*/
async function sendPasswordResetEmail(email, token, baseUrl) {
const resetUrl = `${baseUrl}/auth/reset?email=${encodeURIComponent(email)}&token=${token}`;
const appName = process.env.ZEN_NAME || 'ZEN';
const html = await renderPasswordResetEmail(resetUrl, email, appName);
const result = await sendAuthEmail({
to: email,
subject: `Réinitialisation du mot de passe ${appName}`,
html
});
const resetUrl = `${baseUrl}/auth/reset?email=${encodeURIComponent(email)}&token=${token}`;
const html = await render(<PasswordResetEmail resetUrl={resetUrl} companyName={appName} />);
const result = await sendEmail({ to: email, subject: `Réinitialisation du mot de passe ${appName}`, html });
if (!result.success) {
fail(`Auth: failed to send password reset email to ${email}: ${result.error}`);
throw new Error('Failed to send password reset email');
}
info(`Auth: password reset email sent to ${email}`);
return result;
}
/**
* Send password changed confirmation email using Resend
* @param {string} email - User email
*/
async function sendPasswordChangedEmail(email) {
const appName = process.env.ZEN_NAME || 'ZEN';
const html = await renderPasswordChangedEmail(email, appName);
const result = await sendAuthEmail({
to: email,
subject: `Mot de passe modifié ${appName}`,
html
});
const html = await render(<PasswordChangedEmail email={email} companyName={appName} />);
const result = await sendEmail({ to: email, subject: `Mot de passe modifié ${appName}`, html });
if (!result.success) {
fail(`Auth: failed to send password changed email to ${email}: ${result.error}`);
throw new Error('Failed to send password changed email');
}
info(`Auth: password changed email sent to ${email}`);
return result;
}
@@ -0,0 +1,28 @@
import { Section, Text } from "@react-email/components";
import { BaseLayout } from "../../core/email/templates/BaseLayout.jsx";
export const PasswordChangedEmail = ({ email, companyName }) => (
<BaseLayout
preview={`Votre mot de passe a été modifié ${companyName}`}
title="Mot de passe modifié"
companyName={companyName}
supportSection={true}
>
<Text className="text-[14px] leading-[24px] text-neutral-600 mt-[4px] mb-[24px]">
Ceci confirme que le mot de passe de votre compte <span className="font-medium text-neutral-900">{companyName}</span> a bien été modifié.
</Text>
<Section style={{ border: '1px solid #E5E5E5' }} className="bg-neutral-100 rounded-[12px] p-[20px] my-[24px]">
<Text className="text-[12px] font-medium text-neutral-400 m-0 mb-[4px] uppercase tracking-wider">
Compte
</Text>
<Text className="text-[14px] font-medium text-neutral-900 m-0">
{email}
</Text>
</Section>
<Text className="text-[12px] leading-[20px] text-neutral-400 m-0">
Si vous n'êtes pas à l'origine de cette modification, contactez immédiatement notre équipe de support.
</Text>
</BaseLayout>
);
@@ -0,0 +1,35 @@
import { Button, Section, Text, Link } from "@react-email/components";
import { BaseLayout } from "../../core/email/templates/BaseLayout.jsx";
export const PasswordResetEmail = ({ resetUrl, companyName }) => (
<BaseLayout
preview={`Réinitialisez votre mot de passe pour ${companyName}`}
title="Réinitialisation du mot de passe"
companyName={companyName}
supportSection={true}
>
<Text className="text-[14px] leading-[24px] text-neutral-600 mt-[4px] mb-[24px]">
Nous avons reçu une demande de réinitialisation du mot de passe pour votre compte <span className="font-medium text-neutral-900">{companyName}</span>. Cliquez sur le bouton ci-dessous pour en choisir un nouveau.
</Text>
<Section className="mt-[28px] mb-[32px]">
<Button
href={resetUrl}
className="bg-neutral-900 rounded-[8px] text-white font-medium px-[20px] py-[11px] no-underline text-[13px]"
>
Réinitialiser le mot de passe
</Button>
</Section>
<Text className="text-[12px] leading-[20px] text-neutral-400 m-0">
Ce lien expire dans 1 heure. Si vous n'êtes pas à l'origine de cette demande, ignorez ce message votre mot de passe ne sera pas modifié.
</Text>
<Text className="text-[12px] leading-[20px] text-neutral-400 m-0 mt-[8px]">
Lien :{' '}
<Link href={resetUrl} className="text-neutral-400 underline break-all">
{resetUrl}
</Link>
</Text>
</BaseLayout>
);
@@ -0,0 +1,35 @@
import { Button, Section, Text, Link } from "@react-email/components";
import { BaseLayout } from "../../core/email/templates/BaseLayout.jsx";
export const VerificationEmail = ({ verificationUrl, companyName }) => (
<BaseLayout
preview={`Confirmez votre adresse courriel pour ${companyName}`}
title="Confirmez votre adresse courriel"
companyName={companyName}
supportSection={true}
>
<Text className="text-[14px] leading-[24px] text-neutral-600 mt-[4px] mb-[24px]">
Merci de vous être inscrit sur <span className="font-medium text-neutral-900">{companyName}</span>. Cliquez sur le bouton ci-dessous pour confirmer votre adresse courriel.
</Text>
<Section className="mt-[28px] mb-[32px]">
<Button
href={verificationUrl}
className="bg-neutral-900 rounded-[8px] text-white font-medium px-[20px] py-[11px] no-underline text-[13px]"
>
Confirmer mon courriel
</Button>
</Section>
<Text className="text-[12px] leading-[20px] text-neutral-400 m-0">
Ce lien expire dans 24 heures. Si vous n'avez pas créé de compte, ignorez ce message.
</Text>
<Text className="text-[12px] leading-[20px] text-neutral-400 m-0 mt-[8px]">
Lien :{' '}
<Link href={verificationUrl} className="text-neutral-400 underline break-all">
{verificationUrl}
</Link>
</Text>
</BaseLayout>
);