codalware-auth
Version:
Complete authentication system with enterprise security, attack protection, team workspaces, waitlist, billing, UI components, 2FA, and account recovery - production-ready in 5 minutes. Enhanced CLI with verification, rollback, and App Router scaffolding.
346 lines (307 loc) • 14.1 kB
text/typescript
/* eslint-disable @typescript-eslint/ban-ts-comment */
// @ts-nocheck - Template file, not compiled in package build
import type { Transporter } from 'nodemailer';
import { config } from '../../../config';
// Avoid importing specific env constants at module-eval time. Read them lazily
// from the env module inside ensureTransporter so builds/prerenders don't fail
// due to module-top-level environment access.
// Use shared transporter helpers if provided by the host application
import transporterFromConfig, { ensureTransporter as ensureConfigTransporter } from '../../../config/nodemailer';
interface EmailOptions {
to: string;
subject: string;
html: string;
text?: string;
}
export class EmailService {
// transporter is created lazily to avoid throwing or performing I/O at module
// import time (which can happen during prerender/build on hosts).
private transporter: Transporter | null = null;
constructor() {
// If project provided a prebuilt transporter, keep it for later use.
if (transporterFromConfig) {
this.transporter = transporterFromConfig as Transporter;
}
// Otherwise do not attempt to require/create nodemailer here; postpone it
// until an email needs to be sent so builds/prerenders don't fail.
}
private async ensureTransporter(): Promise<Transporter | null> {
if (this.transporter) return this.transporter;
// If a shared transporter exists on config, use it.
if (transporterFromConfig) {
this.transporter = transporterFromConfig as Transporter;
return this.transporter;
}
if (typeof ensureConfigTransporter === 'function') {
const maybeTransporter = ensureConfigTransporter();
if (maybeTransporter) {
this.transporter = maybeTransporter as Transporter;
return this.transporter;
}
}
// Dynamically import nodemailer at runtime to avoid any module-level
// require/import during build/prerender.
try {
const mod = await import('nodemailer');
const nodemailer = (mod as unknown) as typeof import('nodemailer');
// Read env values lazily to avoid import-time side effects.
const envModule = await import('../../../config/env');
const env = (envModule && 'default' in envModule)
? (envModule.default as typeof import('../../../config/env'))
: (envModule as typeof import('../../../config/env'));
const EMAIL_SERVER_PORT = String(env.EMAIL_SERVER_PORT || '587');
const EMAIL_SERVER_HOST = env.EMAIL_SERVER_HOST;
const EMAIL_SERVER_USER = env.EMAIL_SERVER_USER;
const EMAIL_SERVER_PASSWORD = env.EMAIL_SERVER_PASSWORD;
const port = parseInt(EMAIL_SERVER_PORT || '587', 10) || 587;
const transportOptions: Record<string, unknown> = {
host: EMAIL_SERVER_HOST,
port,
secure: EMAIL_SERVER_PORT === '465',
};
if (EMAIL_SERVER_USER) {
(transportOptions as Record<string, unknown>).auth = {
user: EMAIL_SERVER_USER,
pass: EMAIL_SERVER_PASSWORD,
};
}
// Create transporter using the imported nodemailer module. We cast to
// unknown first to avoid a direct any usage; the runtime value is trusted
// to match the nodemailer API.
// eslint-disable-next-line @typescript-eslint/no-explicit-any
this.transporter = (nodemailer as any).createTransport(transportOptions) as Transporter;
return this.transporter;
} catch (err: unknown) {
let message: string;
if (err && typeof err === 'object' && 'message' in err && typeof (err as Record<string, unknown>).message === 'string') {
// safe extraction of message without using `any`
const e = err as Record<string, unknown>;
message = String(e.message);
} else {
message = String(err);
}
console.warn('Email transporter could not be created at runtime:', message);
this.transporter = null;
return null;
}
}
async sendEmail(options: EmailOptions): Promise<boolean> {
try {
const t = await this.ensureTransporter();
if (!t) {
console.error('Email sending failed: no transporter available');
return false;
}
await t.sendMail({
from: config.EMAIL_FROM,
to: options.to,
subject: options.subject,
html: options.html,
text: options.text,
});
return true;
} catch (error) {
console.error('Email sending failed:', error);
return false;
}
}
async sendVerificationEmail(email: string, token: string, tenantDomain?: string, code?: string): Promise<boolean> {
const baseUrl = tenantDomain ? `https://${tenantDomain}` : config.APP_URL;
const verificationUrl = `${baseUrl}/verify-email?token=${token}&email=${encodeURIComponent(email)}`;
const html = `
<html>
<head>
<meta charset="utf-8">
<title>Verify Your Email</title>
<style>
body { font-family: Arial, sans-serif; line-height: 1.6; color: #333; }
.container { max-width: 600px; margin: 0 auto; padding: 20px; }
.header { background: #3b82f6; color: white; padding: 20px; text-align: center; }
.content { padding: 20px; background: #f9f9f9; }
.button { display: inline-block; padding: 12px 24px; background: #3b82f6; color: white; text-decoration: none; border-radius: 4px; margin: 20px 0; }
.footer { padding: 20px; text-align: center; font-size: 12px; color: #666; }
</style>
</head>
<body>
<div class="container">
<div class="header">
<h1>${config.APP_NAME}</h1>
</div>
<div class="content">
<h2>Verify Your Email Address</h2>
<p>Thank you for signing up! Please click the button below to verify your email address:</p>
<a href="${verificationUrl}" class="button">Verify Email</a>
<p>Or copy and paste this link into your browser:</p>
<p><a href="${verificationUrl}">${verificationUrl}</a></p>
${code ? `<p>For your convenience, you can also enter the following code inside the application:</p>
<p style="font-size: 24px; letter-spacing: 4px; font-weight: bold;">${code}</p>` : ''}
<p>This link will expire in 24 hours.</p>
</div>
<div class="footer">
<p>If you didn't request this verification, please ignore this email.</p>
</div>
</div>
</body>
</html>
`;
const text = `
Verify Your Email Address
Thank you for signing up for ${config.APP_NAME}!
Please click the link below to verify your email address:
${verificationUrl}
${code ? `Or enter this verification code inside the application:
${code}
` : ''}
This link will expire in 24 hours.
If you didn't request this verification, please ignore this email.
`;
return this.sendEmail({
to: email,
subject: `Verify your email for ${config.APP_NAME}`,
html,
text,
});
}
async sendPasswordResetEmail(email: string, token: string, tenantDomain?: string): Promise<boolean> {
const baseUrl = tenantDomain ? `https://${tenantDomain}` : config.APP_URL;
const resetUrl = `${baseUrl}/auth/reset-password?token=${token}&email=${encodeURIComponent(email)}`;
const html = `
<html>
<head>
<meta charset="utf-8">
<title>Reset Your Password</title>
<style>
body { font-family: Arial, sans-serif; line-height: 1.6; color: #333; }
.container { max-width: 600px; margin: 0 auto; padding: 20px; }
.header { background: #dc2626; color: white; padding: 20px; text-align: center; }
.content { padding: 20px; background: #f9f9f9; }
.button { display: inline-block; padding: 12px 24px; background: #dc2626; color: white; text-decoration: none; border-radius: 4px; margin: 20px 0; }
.footer { padding: 20px; text-align: center; font-size: 12px; color: #666; }
</style>
</head>
<body>
<div class="container">
<div class="header">
<h1>${config.APP_NAME}</h1>
</div>
<div class="content">
<h2>Reset Your Password</h2>
<p>You requested a password reset. Click the button below to set a new password:</p>
<a href="${resetUrl}" class="button">Reset Password</a>
<p>Or copy and paste this link into your browser:</p>
<p><a href="${resetUrl}">${resetUrl}</a></p>
<p>This link will expire in 1 hour.</p>
</div>
<div class="footer">
<p>If you didn't request a password reset, please ignore this email.</p>
</div>
</div>
</body>
</html>
`;
const text = `
Reset Your Password
You requested a password reset for your ${config.APP_NAME} account.
Please click the link below to set a new password:
${resetUrl}
This link will expire in 1 hour.
If you didn't request a password reset, please ignore this email.
`;
return this.sendEmail({
to: email,
subject: `Reset your password for ${config.APP_NAME}`,
html,
text,
});
}
async sendRestoreEmail(email: string, token: string, tenantDomain?: string): Promise<boolean> {
const baseUrl = tenantDomain ? `https://${tenantDomain}` : config.APP_URL;
const restoreUrl = `${baseUrl}/auth/restore?token=${token}&email=${encodeURIComponent(email)}`;
const html = `
<html>
<head>
<meta charset="utf-8">
<title>Restore Your Account</title>
<style>
body { font-family: Arial, sans-serif; line-height: 1.6; color: #333; }
.container { max-width: 600px; margin: 0 auto; padding: 20px; }
.header { background: #f59e0b; color: white; padding: 20px; text-align: center; }
.content { padding: 20px; background: #f9f9f9; }
.button { display: inline-block; padding: 12px 24px; background: #f59e0b; color: white; text-decoration: none; border-radius: 4px; margin: 20px 0; }
.footer { padding: 20px; text-align: center; font-size: 12px; color: #666; }
</style>
</head>
<body>
<div class="container">
<div class="header">
<h1>${config.APP_NAME}</h1>
</div>
<div class="content">
<h2>Restore Your Account</h2>
<p>We received a request to restore your account. Click the button below to proceed. This link will expire in 24 hours.</p>
<a href="${restoreUrl}" class="button">Restore Account</a>
<p>Or copy and paste this link into your browser:</p>
<p><a href="${restoreUrl}">${restoreUrl}</a></p>
</div>
<div class="footer">
<p>If you didn't request this, please ignore this email.</p>
</div>
</div>
</body>
</html>
`;
const text = `Restore your account: ${restoreUrl}`;
return this.sendEmail({ to: email, subject: `Restore your ${config.APP_NAME} account`, html, text });
}
async sendApprovalNotification(email: string, approved: boolean, reason?: string, tenantName?: string): Promise<boolean> {
const subject = approved
? `Your account has been approved${tenantName ? ` for ${tenantName}` : ''}`
: `Your account application has been rejected${tenantName ? ` for ${tenantName}` : ''}`;
const html = `
<html>
<head>
<meta charset="utf-8">
<title>${subject}</title>
<style>
body { font-family: Arial, sans-serif; line-height: 1.6; color: #333; }
.container { max-width: 600px; margin: 0 auto; padding: 20px; }
.header { background: ${approved ? '#059669' : '#dc2626'}; color: white; padding: 20px; text-align: center; }
.content { padding: 20px; background: #f9f9f9; }
.button { display: inline-block; padding: 12px 24px; background: #3b82f6; color: white; text-decoration: none; border-radius: 4px; margin: 20px 0; }
.footer { padding: 20px; text-align: center; font-size: 12px; color: #666; }
</style>
</head>
<body>
<div class="container">
<div class="header">
<h1>${config.APP_NAME}</h1>
</div>
<div class="content">
<h2>${approved ? 'Account Approved!' : 'Account Application Update'}</h2>
${approved
? `<p>Great news! Your account has been approved${tenantName ? ` for ${tenantName}` : ''}. You can now log in and start using the platform.</p>
<a href="${config.APP_URL}/auth/login" class="button">Login Now</a>`
: `<p>We regret to inform you that your account application${tenantName ? ` for ${tenantName}` : ''} has been rejected.</p>
${reason ? `<p><strong>Reason:</strong> ${reason}</p>` : ''}
<p>If you believe this is a mistake, please contact support.</p>`
}
</div>
<div class="footer">
<p>Thank you for your interest in ${config.APP_NAME}.</p>
</div>
</div>
</body>
</html>
`;
return this.sendEmail({
to: email,
subject,
html,
});
}
}
export const emailService = new EmailService();