Yaltopia-Ticket-Email/server.ts

356 lines
9.7 KiB
TypeScript

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<void>) =>
(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