UNPKG

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
/* 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 = ` <!DOCTYPE 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 = ` <!DOCTYPE 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 = ` <!DOCTYPE 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 = ` <!DOCTYPE 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();