import { Hono } from 'hono'; import { db, dbGet, dbAll, emailTemplates, emailLogs, events, tickets } from '../db/index.js'; import { eq, desc, and, sql } from 'drizzle-orm'; import { requireAuth } from '../lib/auth.js'; import { getNow, generateId } from '../lib/utils.js'; 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 dbAll( (db as any).select().from(emailTemplates).orderBy(desc((emailTemplates as any).createdAt)) ); // 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 dbGet( (db as any) .select() .from(emailTemplates) .where(eq((emailTemplates as any).id, id)) ); 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 dbGet( (db as any).select().from(emailTemplates).where(eq((emailTemplates as any).slug, slug)) ); if (existing) { return c.json({ error: 'Template with this slug already exists' }, 400); } const now = getNow(); const template = { id: generateId(), 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 dbGet( (db as any) .select() .from(emailTemplates) .where(eq((emailTemplates as any).id, id)) ); 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 dbGet( (db as any) .select() .from(emailTemplates) .where(eq((emailTemplates as any).id, id)) ); 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 dbGet( (db as any).select().from(emailTemplates).where(eq((emailTemplates as any).id, id)) ); 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 dbAll( query .orderBy(desc((emailLogs as any).createdAt)) .limit(limit) .offset(offset) ); // Get total count let countQuery = (db as any) .select({ count: sql`count(*)` }) .from(emailLogs); if (conditions.length > 0) { countQuery = countQuery.where(and(...conditions)); } const totalResult = await dbGet(countQuery); 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 dbGet( (db as any).select().from(emailLogs).where(eq((emailLogs as any).id, id)) ); 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`count(*)` }).from(emailLogs).where(baseCondition) : (db as any).select({ count: sql`count(*)` }).from(emailLogs); const total = (await dbGet(totalQuery))?.count || 0; const sentCondition = baseCondition ? and(baseCondition, eq((emailLogs as any).status, 'sent')) : eq((emailLogs as any).status, 'sent'); const sent = (await dbGet((db as any).select({ count: sql`count(*)` }).from(emailLogs).where(sentCondition)))?.count || 0; const failedCondition = baseCondition ? and(baseCondition, eq((emailLogs as any).status, 'failed')) : eq((emailLogs as any).status, 'failed'); const failed = (await dbGet((db as any).select({ count: sql`count(*)` }).from(emailLogs).where(failedCondition)))?.count || 0; const pendingCondition = baseCondition ? and(baseCondition, eq((emailLogs as any).status, 'pending')) : eq((emailLogs as any).status, 'pending'); const pending = (await dbGet((db as any).select({ count: sql`count(*)` }).from(emailLogs).where(pendingCondition)))?.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;