first commit
This commit is contained in:
419
backend/src/routes/emails.ts
Normal file
419
backend/src/routes/emails.ts
Normal file
@@ -0,0 +1,419 @@
|
||||
import { Hono } from 'hono';
|
||||
import { db, emailTemplates, emailLogs, events, tickets } from '../db/index.js';
|
||||
import { eq, desc, and, sql } from 'drizzle-orm';
|
||||
import { requireAuth } from '../lib/auth.js';
|
||||
import { getNow } from '../lib/utils.js';
|
||||
import { nanoid } from 'nanoid';
|
||||
import emailService from '../lib/email.js';
|
||||
import { getTemplateVariables, defaultTemplates } from '../lib/emailTemplates.js';
|
||||
|
||||
const emailsRouter = new Hono();
|
||||
|
||||
// ==================== Template Routes ====================
|
||||
|
||||
// Get all email templates
|
||||
emailsRouter.get('/templates', requireAuth(['admin', 'organizer']), async (c) => {
|
||||
const templates = await (db as any)
|
||||
.select()
|
||||
.from(emailTemplates)
|
||||
.orderBy(desc((emailTemplates as any).createdAt))
|
||||
.all();
|
||||
|
||||
// Parse variables JSON for each template
|
||||
const parsedTemplates = templates.map((t: any) => ({
|
||||
...t,
|
||||
variables: t.variables ? JSON.parse(t.variables) : [],
|
||||
isSystem: Boolean(t.isSystem),
|
||||
isActive: Boolean(t.isActive),
|
||||
}));
|
||||
|
||||
return c.json({ templates: parsedTemplates });
|
||||
});
|
||||
|
||||
// Get single email template
|
||||
emailsRouter.get('/templates/:id', requireAuth(['admin', 'organizer']), async (c) => {
|
||||
const { id } = c.req.param();
|
||||
|
||||
const template = await (db as any)
|
||||
.select()
|
||||
.from(emailTemplates)
|
||||
.where(eq((emailTemplates as any).id, id))
|
||||
.get();
|
||||
|
||||
if (!template) {
|
||||
return c.json({ error: 'Template not found' }, 404);
|
||||
}
|
||||
|
||||
return c.json({
|
||||
template: {
|
||||
...template,
|
||||
variables: template.variables ? JSON.parse(template.variables) : [],
|
||||
isSystem: Boolean(template.isSystem),
|
||||
isActive: Boolean(template.isActive),
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
// Create new email template
|
||||
emailsRouter.post('/templates', requireAuth(['admin']), async (c) => {
|
||||
const body = await c.req.json();
|
||||
const { name, slug, subject, subjectEs, bodyHtml, bodyHtmlEs, bodyText, bodyTextEs, description, variables } = body;
|
||||
|
||||
if (!name || !slug || !subject || !bodyHtml) {
|
||||
return c.json({ error: 'Name, slug, subject, and bodyHtml are required' }, 400);
|
||||
}
|
||||
|
||||
// Check if slug already exists
|
||||
const existing = await (db as any)
|
||||
.select()
|
||||
.from(emailTemplates)
|
||||
.where(eq((emailTemplates as any).slug, slug))
|
||||
.get();
|
||||
|
||||
if (existing) {
|
||||
return c.json({ error: 'Template with this slug already exists' }, 400);
|
||||
}
|
||||
|
||||
const now = getNow();
|
||||
const template = {
|
||||
id: nanoid(),
|
||||
name,
|
||||
slug,
|
||||
subject,
|
||||
subjectEs: subjectEs || null,
|
||||
bodyHtml,
|
||||
bodyHtmlEs: bodyHtmlEs || null,
|
||||
bodyText: bodyText || null,
|
||||
bodyTextEs: bodyTextEs || null,
|
||||
description: description || null,
|
||||
variables: variables ? JSON.stringify(variables) : null,
|
||||
isSystem: 0,
|
||||
isActive: 1,
|
||||
createdAt: now,
|
||||
updatedAt: now,
|
||||
};
|
||||
|
||||
await (db as any).insert(emailTemplates).values(template);
|
||||
|
||||
return c.json({
|
||||
template: {
|
||||
...template,
|
||||
variables: variables || [],
|
||||
isSystem: false,
|
||||
isActive: true,
|
||||
},
|
||||
message: 'Template created successfully'
|
||||
}, 201);
|
||||
});
|
||||
|
||||
// Update email template
|
||||
emailsRouter.put('/templates/:id', requireAuth(['admin']), async (c) => {
|
||||
const { id } = c.req.param();
|
||||
const body = await c.req.json();
|
||||
|
||||
const existing = await (db as any)
|
||||
.select()
|
||||
.from(emailTemplates)
|
||||
.where(eq((emailTemplates as any).id, id))
|
||||
.get();
|
||||
|
||||
if (!existing) {
|
||||
return c.json({ error: 'Template not found' }, 404);
|
||||
}
|
||||
|
||||
const updateData: any = { updatedAt: getNow() };
|
||||
|
||||
// Only allow updating certain fields for system templates
|
||||
const systemProtectedFields = ['slug', 'isSystem'];
|
||||
|
||||
const allowedFields = ['name', 'subject', 'subjectEs', 'bodyHtml', 'bodyHtmlEs', 'bodyText', 'bodyTextEs', 'description', 'variables', 'isActive'];
|
||||
if (!existing.isSystem) {
|
||||
allowedFields.push('slug');
|
||||
}
|
||||
|
||||
for (const field of allowedFields) {
|
||||
if (body[field] !== undefined) {
|
||||
if (field === 'variables') {
|
||||
updateData[field] = JSON.stringify(body[field]);
|
||||
} else if (field === 'isActive') {
|
||||
updateData[field] = body[field] ? 1 : 0;
|
||||
} else {
|
||||
updateData[field] = body[field];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
await (db as any)
|
||||
.update(emailTemplates)
|
||||
.set(updateData)
|
||||
.where(eq((emailTemplates as any).id, id));
|
||||
|
||||
const updated = await (db as any)
|
||||
.select()
|
||||
.from(emailTemplates)
|
||||
.where(eq((emailTemplates as any).id, id))
|
||||
.get();
|
||||
|
||||
return c.json({
|
||||
template: {
|
||||
...updated,
|
||||
variables: updated.variables ? JSON.parse(updated.variables) : [],
|
||||
isSystem: Boolean(updated.isSystem),
|
||||
isActive: Boolean(updated.isActive),
|
||||
},
|
||||
message: 'Template updated successfully'
|
||||
});
|
||||
});
|
||||
|
||||
// Delete email template (only non-system templates)
|
||||
emailsRouter.delete('/templates/:id', requireAuth(['admin']), async (c) => {
|
||||
const { id } = c.req.param();
|
||||
|
||||
const template = await (db as any)
|
||||
.select()
|
||||
.from(emailTemplates)
|
||||
.where(eq((emailTemplates as any).id, id))
|
||||
.get();
|
||||
|
||||
if (!template) {
|
||||
return c.json({ error: 'Template not found' }, 404);
|
||||
}
|
||||
|
||||
if (template.isSystem) {
|
||||
return c.json({ error: 'Cannot delete system templates' }, 400);
|
||||
}
|
||||
|
||||
await (db as any)
|
||||
.delete(emailTemplates)
|
||||
.where(eq((emailTemplates as any).id, id));
|
||||
|
||||
return c.json({ message: 'Template deleted successfully' });
|
||||
});
|
||||
|
||||
// Get available template variables
|
||||
emailsRouter.get('/templates/:slug/variables', requireAuth(['admin', 'organizer']), async (c) => {
|
||||
const { slug } = c.req.param();
|
||||
const variables = getTemplateVariables(slug);
|
||||
return c.json({ variables });
|
||||
});
|
||||
|
||||
// ==================== Email Sending Routes ====================
|
||||
|
||||
// Send email using template to event attendees
|
||||
emailsRouter.post('/send/event/:eventId', requireAuth(['admin', 'organizer']), async (c) => {
|
||||
const { eventId } = c.req.param();
|
||||
const user = (c as any).get('user');
|
||||
const body = await c.req.json();
|
||||
const { templateSlug, customVariables, recipientFilter } = body;
|
||||
|
||||
if (!templateSlug) {
|
||||
return c.json({ error: 'Template slug is required' }, 400);
|
||||
}
|
||||
|
||||
const result = await emailService.sendToEventAttendees({
|
||||
eventId,
|
||||
templateSlug,
|
||||
customVariables,
|
||||
recipientFilter: recipientFilter || 'confirmed',
|
||||
sentBy: user?.id,
|
||||
});
|
||||
|
||||
return c.json(result);
|
||||
});
|
||||
|
||||
// Send custom email to specific recipients
|
||||
emailsRouter.post('/send/custom', requireAuth(['admin', 'organizer']), async (c) => {
|
||||
const user = (c as any).get('user');
|
||||
const body = await c.req.json();
|
||||
const { to, toName, subject, bodyHtml, bodyText, eventId } = body;
|
||||
|
||||
if (!to || !subject || !bodyHtml) {
|
||||
return c.json({ error: 'Recipient (to), subject, and bodyHtml are required' }, 400);
|
||||
}
|
||||
|
||||
const result = await emailService.sendCustomEmail({
|
||||
to,
|
||||
toName,
|
||||
subject,
|
||||
bodyHtml,
|
||||
bodyText,
|
||||
eventId,
|
||||
sentBy: user?.id,
|
||||
});
|
||||
|
||||
return c.json(result);
|
||||
});
|
||||
|
||||
// Preview email (render template without sending)
|
||||
emailsRouter.post('/preview', requireAuth(['admin', 'organizer']), async (c) => {
|
||||
const body = await c.req.json();
|
||||
const { templateSlug, variables, locale } = body;
|
||||
|
||||
if (!templateSlug) {
|
||||
return c.json({ error: 'Template slug is required' }, 400);
|
||||
}
|
||||
|
||||
const template = await emailService.getTemplate(templateSlug);
|
||||
if (!template) {
|
||||
return c.json({ error: 'Template not found' }, 404);
|
||||
}
|
||||
|
||||
const { replaceTemplateVariables, wrapInBaseTemplate } = await import('../lib/emailTemplates.js');
|
||||
|
||||
const allVariables = {
|
||||
...emailService.getCommonVariables(),
|
||||
lang: locale || 'en',
|
||||
...variables,
|
||||
};
|
||||
|
||||
const subject = locale === 'es' && template.subjectEs
|
||||
? template.subjectEs
|
||||
: template.subject;
|
||||
const bodyHtml = locale === 'es' && template.bodyHtmlEs
|
||||
? template.bodyHtmlEs
|
||||
: template.bodyHtml;
|
||||
|
||||
const finalSubject = replaceTemplateVariables(subject, allVariables);
|
||||
const finalBodyContent = replaceTemplateVariables(bodyHtml, allVariables);
|
||||
const finalBodyHtml = wrapInBaseTemplate(finalBodyContent, { ...allVariables, subject: finalSubject });
|
||||
|
||||
return c.json({
|
||||
subject: finalSubject,
|
||||
bodyHtml: finalBodyHtml,
|
||||
});
|
||||
});
|
||||
|
||||
// ==================== Email Logs Routes ====================
|
||||
|
||||
// Get email logs
|
||||
emailsRouter.get('/logs', requireAuth(['admin', 'organizer']), async (c) => {
|
||||
const eventId = c.req.query('eventId');
|
||||
const status = c.req.query('status');
|
||||
const limit = parseInt(c.req.query('limit') || '50');
|
||||
const offset = parseInt(c.req.query('offset') || '0');
|
||||
|
||||
let query = (db as any).select().from(emailLogs);
|
||||
|
||||
const conditions = [];
|
||||
if (eventId) {
|
||||
conditions.push(eq((emailLogs as any).eventId, eventId));
|
||||
}
|
||||
if (status) {
|
||||
conditions.push(eq((emailLogs as any).status, status));
|
||||
}
|
||||
|
||||
if (conditions.length > 0) {
|
||||
query = query.where(and(...conditions));
|
||||
}
|
||||
|
||||
const logs = await query
|
||||
.orderBy(desc((emailLogs as any).createdAt))
|
||||
.limit(limit)
|
||||
.offset(offset)
|
||||
.all();
|
||||
|
||||
// Get total count
|
||||
let countQuery = (db as any)
|
||||
.select({ count: sql<number>`count(*)` })
|
||||
.from(emailLogs);
|
||||
|
||||
if (conditions.length > 0) {
|
||||
countQuery = countQuery.where(and(...conditions));
|
||||
}
|
||||
|
||||
const totalResult = await countQuery.get();
|
||||
const total = totalResult?.count || 0;
|
||||
|
||||
return c.json({
|
||||
logs,
|
||||
pagination: {
|
||||
total,
|
||||
limit,
|
||||
offset,
|
||||
hasMore: offset + logs.length < total,
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
// Get single email log
|
||||
emailsRouter.get('/logs/:id', requireAuth(['admin', 'organizer']), async (c) => {
|
||||
const { id } = c.req.param();
|
||||
|
||||
const log = await (db as any)
|
||||
.select()
|
||||
.from(emailLogs)
|
||||
.where(eq((emailLogs as any).id, id))
|
||||
.get();
|
||||
|
||||
if (!log) {
|
||||
return c.json({ error: 'Email log not found' }, 404);
|
||||
}
|
||||
|
||||
return c.json({ log });
|
||||
});
|
||||
|
||||
// Get email stats
|
||||
emailsRouter.get('/stats', requireAuth(['admin', 'organizer']), async (c) => {
|
||||
const eventId = c.req.query('eventId');
|
||||
|
||||
let baseCondition = eventId ? eq((emailLogs as any).eventId, eventId) : undefined;
|
||||
|
||||
const totalQuery = baseCondition
|
||||
? (db as any).select({ count: sql<number>`count(*)` }).from(emailLogs).where(baseCondition)
|
||||
: (db as any).select({ count: sql<number>`count(*)` }).from(emailLogs);
|
||||
|
||||
const total = (await totalQuery.get())?.count || 0;
|
||||
|
||||
const sentCondition = baseCondition
|
||||
? and(baseCondition, eq((emailLogs as any).status, 'sent'))
|
||||
: eq((emailLogs as any).status, 'sent');
|
||||
const sent = (await (db as any).select({ count: sql<number>`count(*)` }).from(emailLogs).where(sentCondition).get())?.count || 0;
|
||||
|
||||
const failedCondition = baseCondition
|
||||
? and(baseCondition, eq((emailLogs as any).status, 'failed'))
|
||||
: eq((emailLogs as any).status, 'failed');
|
||||
const failed = (await (db as any).select({ count: sql<number>`count(*)` }).from(emailLogs).where(failedCondition).get())?.count || 0;
|
||||
|
||||
const pendingCondition = baseCondition
|
||||
? and(baseCondition, eq((emailLogs as any).status, 'pending'))
|
||||
: eq((emailLogs as any).status, 'pending');
|
||||
const pending = (await (db as any).select({ count: sql<number>`count(*)` }).from(emailLogs).where(pendingCondition).get())?.count || 0;
|
||||
|
||||
return c.json({
|
||||
stats: {
|
||||
total,
|
||||
sent,
|
||||
failed,
|
||||
pending,
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
// Seed default templates (admin only)
|
||||
emailsRouter.post('/seed-templates', requireAuth(['admin']), async (c) => {
|
||||
await emailService.seedDefaultTemplates();
|
||||
return c.json({ message: 'Default templates seeded successfully' });
|
||||
});
|
||||
|
||||
// ==================== Configuration Routes ====================
|
||||
|
||||
// Get email provider info
|
||||
emailsRouter.get('/config', requireAuth(['admin']), async (c) => {
|
||||
const providerInfo = emailService.getProviderInfo();
|
||||
return c.json(providerInfo);
|
||||
});
|
||||
|
||||
// Test email configuration
|
||||
emailsRouter.post('/test', requireAuth(['admin']), async (c) => {
|
||||
const body = await c.req.json();
|
||||
const { to } = body;
|
||||
|
||||
if (!to) {
|
||||
return c.json({ error: 'Recipient email (to) is required' }, 400);
|
||||
}
|
||||
|
||||
const result = await emailService.testConnection(to);
|
||||
return c.json(result);
|
||||
});
|
||||
|
||||
export default emailsRouter;
|
||||
Reference in New Issue
Block a user