import { Router } from 'express'; import { emailTemplatesService } from './email-templates.service'; import { emailService } from '../../services/email.service'; import { validate } from '../../middleware/validate'; import { listEmailTemplatesSchema, createEmailTemplateSchema, updateEmailTemplateSchema, rollbackToVersionSchema, validateTemplateSchema, sendTestEmailSchema, } from './email-templates.schemas'; import { logger } from '../../utils/logger'; import { authenticate } from '../../middleware/auth.middleware'; import { requireRole } from '../../middleware/rbac.middleware'; import { UserRole } from '@prisma/client'; import { Request, Response } from 'express'; import rateLimit from 'express-rate-limit'; import RedisStore from 'rate-limit-redis'; import { redis } from '../../config/redis'; const router = Router(); // All email template routes require authentication router.use(authenticate); // All routes require admin role (SUPER_ADMIN, INFLUENCE_ADMIN, or MAP_ADMIN) const requireAdminRole = requireRole(UserRole.SUPER_ADMIN, UserRole.INFLUENCE_ADMIN, UserRole.MAP_ADMIN); /** * List email templates * GET /email-templates */ router.get( '/', requireAdminRole, validate(listEmailTemplatesSchema, 'query'), async (req: Request, res: Response): Promise => { try { const result = await emailTemplatesService.list(req.query as any); res.json(result); } catch (error) { logger.error('Error listing email templates:', error); res.status(500).json({ error: 'Failed to list email templates' }); } }, ); /** * Get single email template * GET /email-templates/:id */ router.get( '/:id', requireAdminRole, async (req: Request, res: Response): Promise => { try { const template = await emailTemplatesService.getById(req.params.id as string); res.json(template); } catch (error) { if (error instanceof Error && error.message === 'Template not found') { res.status(404).json({ error: 'Template not found' }); return; } logger.error('Error getting email template:', error); res.status(500).json({ error: 'Failed to get email template' }); } }, ); /** * Create email template * POST /email-templates */ router.post( '/', requireAdminRole, validate(createEmailTemplateSchema), async (req: Request, res: Response): Promise => { try { const template = await emailTemplatesService.create(req.body, req.user!.id); res.status(201).json(template); } catch (error) { if (error instanceof Error && error.message.includes('already exists')) { res.status(409).json({ error: error.message }); return; } if (error instanceof Error && error.message.includes('validation failed')) { res.status(400).json({ error: error.message }); return; } logger.error('Error creating email template:', error); res.status(500).json({ error: 'Failed to create email template' }); } }, ); /** * Update email template * PUT /email-templates/:id */ router.put( '/:id', requireAdminRole, validate(updateEmailTemplateSchema), async (req: Request, res: Response): Promise => { try { const template = await emailTemplatesService.update(req.params.id as string, req.body, req.user!.id); // Clear cache so changes take effect immediately emailService.clearDatabaseCache(template.key); logger.info(`Cleared template cache for: ${template.key}`); res.json(template); } catch (error) { if (error instanceof Error && error.message === 'Template not found') { res.status(404).json({ error: 'Template not found' }); return; } if (error instanceof Error && error.message.includes('validation failed')) { res.status(400).json({ error: error.message }); return; } logger.error('Error updating email template:', error); res.status(500).json({ error: 'Failed to update email template' }); } }, ); /** * Delete email template * DELETE /email-templates/:id */ router.delete( '/:id', requireAdminRole, async (req: Request, res: Response): Promise => { try { // Fetch template before deleting to get the key const template = await emailTemplatesService.getById(req.params.id as string); await emailTemplatesService.delete(req.params.id as string); // Clear cache for deleted template emailService.clearDatabaseCache(template.key); logger.info(`Cleared template cache for deleted template: ${template.key}`); res.status(204).send(); } catch (error) { if (error instanceof Error && error.message === 'Template not found') { res.status(404).json({ error: 'Template not found' }); return; } if (error instanceof Error && error.message.includes('Cannot delete system templates')) { res.status(403).json({ error: error.message }); return; } logger.error('Error deleting email template:', error); res.status(500).json({ error: 'Failed to delete email template' }); } }, ); /** * Get version history * GET /email-templates/:id/versions */ router.get( '/:id/versions', requireAdminRole, async (req: Request, res: Response): Promise => { try { const versions = await emailTemplatesService.getVersions(req.params.id as string); res.json(versions); } catch (error) { logger.error('Error getting template versions:', error); res.status(500).json({ error: 'Failed to get template versions' }); } }, ); /** * Get specific version * GET /email-templates/:id/versions/:versionNumber */ router.get( '/:id/versions/:versionNumber', requireAdminRole, async (req: Request, res: Response): Promise => { try { const version = await emailTemplatesService.getVersion( req.params.id as string, parseInt(req.params.versionNumber as string, 10), ); res.json(version); } catch (error) { if (error instanceof Error && error.message === 'Version not found') { res.status(404).json({ error: 'Version not found' }); return; } logger.error('Error getting template version:', error); res.status(500).json({ error: 'Failed to get template version' }); } }, ); /** * Rollback to previous version * POST /email-templates/:id/rollback */ router.post( '/:id/rollback', requireAdminRole, validate(rollbackToVersionSchema), async (req: Request, res: Response): Promise => { try { const template = await emailTemplatesService.rollbackToVersion( req.params.id as string, req.body, req.user!.id, ); res.json(template); } catch (error) { if (error instanceof Error && (error.message === 'Template not found' || error.message === 'Version not found')) { res.status(404).json({ error: error.message }); return; } logger.error('Error rolling back template:', error); res.status(500).json({ error: 'Failed to rollback template' }); } }, ); /** * Validate template syntax * POST /email-templates/validate */ router.post( '/validate', requireAdminRole, validate(validateTemplateSchema), async (req: Request, res: Response): Promise => { try { const result = emailTemplatesService.validateTemplate(req.body); res.json(result); } catch (error) { logger.error('Error validating template:', error); res.status(500).json({ error: 'Failed to validate template' }); } }, ); /** * Send test email * POST /email-templates/:id/test * Rate limited to 10 per 15 minutes per user */ router.post( '/:id/test', requireAdminRole, rateLimit({ windowMs: 15 * 60 * 1000, // 15 minutes max: 10, standardHeaders: true, legacyHeaders: false, store: new RedisStore({ sendCommand: (command: string, ...args: string[]) => redis.call(command, ...args) as Promise, prefix: 'rl:email-template-test:', }), keyGenerator: (req) => req.user!.id, }), validate(sendTestEmailSchema), async (req: Request, res: Response): Promise => { try { const result = await emailTemplatesService.sendTestEmail(req.params.id as string, req.body, req.user!.id); res.json(result); } catch (error) { if (error instanceof Error && error.message === 'Template not found') { res.status(404).json({ error: 'Template not found' }); return; } logger.error('Error sending test email:', error); res.status(500).json({ error: 'Failed to send test email' }); } }, ); /** * Get test logs for template * GET /email-templates/:id/test-logs */ router.get( '/:id/test-logs', requireAdminRole, async (req: Request, res: Response): Promise => { try { const limit = req.query.limit ? parseInt(req.query.limit as string, 10) : 10; const logs = await emailTemplatesService.getTestLogs(req.params.id as string, limit); res.json(logs); } catch (error) { logger.error('Error getting test logs:', error); res.status(500).json({ error: 'Failed to get test logs' }); } }, ); /** * Seed templates from filesystem (SUPER_ADMIN only) * POST /email-templates/seed */ router.post( '/seed', requireRole(UserRole.SUPER_ADMIN), async (req: Request, res: Response): Promise => { try { // This is a placeholder - the actual seeding is done via the script // But we keep this endpoint for manual triggering if needed const { spawn } = require('child_process'); const child = spawn('npx', ['tsx', 'src/scripts/seed-email-templates.ts'], { cwd: '/app', shell: false, }); let exitCode = 0; await new Promise((resolve) => { child.on('close', (code: number) => { exitCode = code; resolve(); }); }); if (exitCode !== 0) { throw new Error(`Seed script exited with code ${exitCode}`); } logger.info('Email templates seeded via API'); res.json({ success: true, message: 'Templates seeded successfully' }); } catch (error) { logger.error('Error seeding templates:', error); res.status(500).json({ error: 'Failed to seed templates' }); } }, ); /** * Clear template cache (SUPER_ADMIN only) * POST /email-templates/clear-cache * Body: { key?: string } - Optional template key to clear. If not provided, clears all. */ router.post( '/clear-cache', requireRole(UserRole.SUPER_ADMIN), async (req: Request, res: Response): Promise => { try { const { key } = req.body; emailService.clearDatabaseCache(key); logger.info(`Template cache cleared${key ? ` for: ${key}` : ' (all)'}`); res.json({ success: true, cleared: key || 'all' }); } catch (error) { logger.error('Error clearing template cache:', error); res.status(500).json({ error: 'Failed to clear template cache' }); } }, ); export default router;