import express from 'express' import cors from 'cors' import helmet from 'helmet' import rateLimit from 'express-rate-limit' import { config } from './src/lib/config' import { logger, requestLogger } from './src/lib/logger' import { ipWhitelistAuth } from './src/lib/ipAuth' import { ResendService } from './src/lib/resendService' import { formatValidationError } from './src/lib/validation' import { z } from 'zod' const app = express() // Security middleware app.use(helmet({ contentSecurityPolicy: { directives: { defaultSrc: ["'self'"], scriptSrc: ["'none'"], objectSrc: ["'none'"], styleSrc: ["'self'", "'unsafe-inline'"], // Allow inline styles for emails }, }, hsts: { maxAge: 31536000, includeSubDomains: true, preload: true } })) // CORS configuration app.use(cors({ origin: config.security.corsOrigin, credentials: true, methods: ['GET', 'POST'], allowedHeaders: ['Content-Type', 'Authorization'] })) // Body parsing app.use(express.json({ limit: '1mb' })) app.use(express.urlencoded({ extended: true, limit: '1mb' })) // Request logging app.use(requestLogger) // Rate limiting const emailRateLimit = rateLimit({ windowMs: config.security.rateLimit.windowMs, max: config.security.rateLimit.max, message: { error: 'Too many email requests, please try again later', retryAfter: Math.ceil(config.security.rateLimit.windowMs / 1000) }, standardHeaders: true, legacyHeaders: false, handler: (req, res) => { logger.rateLimitExceeded(req.ip, req.path) res.status(429).json({ error: 'Too many email requests, please try again later', retryAfter: Math.ceil(config.security.rateLimit.windowMs / 1000) }) } }) // Apply rate limiting and IP authentication to email endpoints (restrict to one backend) app.use('/api/emails', emailRateLimit, ipWhitelistAuth) // Initialize email service const emailService = new ResendService() // Error handling middleware const asyncHandler = (fn: (req: express.Request, res: express.Response, next: express.NextFunction) => Promise) => (req: express.Request, res: express.Response, next: express.NextFunction) => { Promise.resolve(fn(req, res, next)).catch(next) } // Health check endpoint app.get('/health', asyncHandler(async (req: express.Request, res: express.Response) => { const health = await emailService.healthCheck() const status = health.status === 'healthy' ? 200 : 503 res.status(status).json({ status: health.status, timestamp: health.timestamp, service: 'email-template-service', version: '1.0.0' }) })) // API info endpoint app.get('/api', (req: express.Request, res: express.Response) => { res.json({ service: 'Email Template Service', version: '1.0.0', endpoints: { 'POST /api/emails/invitation': 'Send invitation email', 'POST /api/emails/payment-request': 'Send payment request email', 'POST /api/emails/enhanced-payment-request': 'Send enhanced payment request email with line items', 'POST /api/emails/team-invitation': 'Send team member invitation email', 'POST /api/emails/invoice-share': 'Send invoice sharing notification email', 'POST /api/emails/password-reset': 'Send password reset email', 'GET /health': 'Health check', }, rateLimit: { max: config.security.rateLimit.max, windowMs: config.security.rateLimit.windowMs } }) }) // Send invitation email app.post('/api/emails/invitation', asyncHandler(async (req: express.Request, res: express.Response) => { try { const result = await emailService.sendInvitation(req.body) if (!result.success) { const status = result.code === 'VALIDATION_ERROR' ? 400 : 500 return res.status(status).json({ success: false, error: result.error, code: result.code }) } res.json({ success: true, messageId: result.id, duration: result.duration }) } catch (error) { logger.error('Invitation email endpoint error', error as Error) res.status(500).json({ success: false, error: 'Internal server error', code: 'INTERNAL_ERROR' }) } })) // Send payment request email app.post('/api/emails/payment-request', asyncHandler(async (req: express.Request, res: express.Response) => { try { const result = await emailService.sendPaymentRequest(req.body) if (!result.success) { const status = result.code === 'VALIDATION_ERROR' ? 400 : 500 return res.status(status).json({ success: false, error: result.error, code: result.code }) } res.json({ success: true, messageId: result.id, duration: result.duration }) } catch (error) { logger.error('Payment request email endpoint error', error as Error) res.status(500).json({ success: false, error: 'Internal server error', code: 'INTERNAL_ERROR' }) } })) // Send password reset email app.post('/api/emails/password-reset', asyncHandler(async (req: express.Request, res: express.Response) => { try { const result = await emailService.sendPasswordReset(req.body) if (!result.success) { const status = result.code === 'VALIDATION_ERROR' ? 400 : 500 return res.status(status).json({ success: false, error: result.error, code: result.code }) } res.json({ success: true, messageId: result.id, duration: result.duration }) } catch (error) { logger.error('Password reset email endpoint error', error as Error) res.status(500).json({ success: false, error: 'Internal server error', code: 'INTERNAL_ERROR' }) } })) // Send team invitation email app.post('/api/emails/team-invitation', asyncHandler(async (req: express.Request, res: express.Response) => { try { const result = await emailService.sendTeamInvitation(req.body) if (!result.success) { const status = result.code === 'VALIDATION_ERROR' ? 400 : 500 return res.status(status).json({ success: false, error: result.error, code: result.code }) } res.json({ success: true, messageId: result.id, duration: result.duration }) } catch (error) { logger.error('Team invitation email endpoint error', error as Error) res.status(500).json({ success: false, error: 'Internal server error', code: 'INTERNAL_ERROR' }) } })) // Send invoice sharing email app.post('/api/emails/invoice-share', asyncHandler(async (req: express.Request, res: express.Response) => { try { const result = await emailService.sendInvoiceShare(req.body) if (!result.success) { const status = result.code === 'VALIDATION_ERROR' ? 400 : 500 return res.status(status).json({ success: false, error: result.error, code: result.code }) } res.json({ success: true, messageId: result.id, duration: result.duration }) } catch (error) { logger.error('Invoice share email endpoint error', error as Error) res.status(500).json({ success: false, error: 'Internal server error', code: 'INTERNAL_ERROR' }) } })) // Send enhanced payment request email app.post('/api/emails/enhanced-payment-request', asyncHandler(async (req: express.Request, res: express.Response) => { try { const result = await emailService.sendEnhancedPaymentRequest(req.body) if (!result.success) { const status = result.code === 'VALIDATION_ERROR' ? 400 : 500 return res.status(status).json({ success: false, error: result.error, code: result.code }) } res.json({ success: true, messageId: result.id, duration: result.duration }) } catch (error) { logger.error('Enhanced payment request email endpoint error', error as Error) res.status(500).json({ success: false, error: 'Internal server error', code: 'INTERNAL_ERROR' }) } })) // Yaltopia API Integration Endpoints // 404 handler app.use('*', (req: express.Request, res: express.Response) => { res.status(404).json({ error: 'Endpoint not found', path: req.originalUrl, method: req.method }) }) // Global error handler // eslint-disable-next-line @typescript-eslint/no-unused-vars app.use((error: Error, req: express.Request, res: express.Response, _next: express.NextFunction) => { logger.error('Unhandled error', error, { path: req.path, method: req.method, body: req.body }) if (error instanceof z.ZodError) { return res.status(400).json({ success: false, error: 'Validation error', details: formatValidationError(error), code: 'VALIDATION_ERROR' }) } if (error.message.includes('Resend')) { return res.status(503).json({ success: false, error: 'Email service temporarily unavailable', code: 'SERVICE_UNAVAILABLE' }) } res.status(500).json({ success: false, error: config.features.enableDetailedErrors ? error.message : 'Internal server error', code: 'INTERNAL_ERROR' }) }) // Graceful shutdown process.on('SIGTERM', () => { logger.info('SIGTERM received, shutting down gracefully') process.exit(0) }) process.on('SIGINT', () => { logger.info('SIGINT received, shutting down gracefully') process.exit(0) }) // Start server const server = app.listen(config.app.port, () => { logger.info('Email service started', { port: config.app.port, nodeEnv: config.app.nodeEnv, fromDomain: config.email.fromDomain, rateLimit: config.security.rateLimit }) }) // Handle server errors server.on('error', (error: Error) => { logger.error('Server error', error) process.exit(1) }) export default app