changemaker.lite/api/src/modules/email-templates/email-templates-admin.routes.ts

357 lines
11 KiB
TypeScript

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<void> => {
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<void> => {
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<void> => {
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<void> => {
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<void> => {
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<void> => {
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<void> => {
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<void> => {
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<void> => {
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<any>,
prefix: 'rl:email-template-test:',
}),
keyGenerator: (req) => req.user!.id,
}),
validate(sendTestEmailSchema),
async (req: Request, res: Response): Promise<void> => {
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<void> => {
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<void> => {
try {
// This is a placeholder - the actual seeding is done via the script
// But we keep this endpoint for manual triggering if needed
const { exec } = require('child_process');
const { promisify } = require('util');
const execAsync = promisify(exec);
const result = await execAsync('npx tsx src/scripts/seed-email-templates.ts', {
cwd: '/app',
});
logger.info('Email templates seeded via API');
res.json({ success: true, output: result.stdout });
} 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<void> => {
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;