Files
Spanglish/backend/src/routes/emails.ts
Michilis bafd1425c4 Add PostgreSQL support with SQLite/Postgres database compatibility layer
- Add dbGet/dbAll helper functions for database-agnostic queries
- Add toDbBool/convertBooleansForDb for boolean type conversion
- Add toDbDate/getNow for timestamp type handling
- Add generateId that returns UUID for Postgres, nanoid for SQLite
- Update all routes to use compatibility helpers
- Add normalizeEvent to return clean number types from Postgres decimal
- Add formatPrice utility for consistent price display
- Add legal pages admin interface with RichTextEditor
- Update carousel images
- Add drizzle migration files for PostgreSQL
2026-02-02 03:46:35 +00:00

415 lines
12 KiB
TypeScript

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<any>(
(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<any>(
(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<any>(
(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<any>(
(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<any>(
(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<any>(
(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<number>`count(*)` })
.from(emailLogs);
if (conditions.length > 0) {
countQuery = countQuery.where(and(...conditions));
}
const totalResult = await dbGet<any>(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<any>(
(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<number>`count(*)` }).from(emailLogs).where(baseCondition)
: (db as any).select({ count: sql<number>`count(*)` }).from(emailLogs);
const total = (await dbGet<any>(totalQuery))?.count || 0;
const sentCondition = baseCondition
? and(baseCondition, eq((emailLogs as any).status, 'sent'))
: eq((emailLogs as any).status, 'sent');
const sent = (await dbGet<any>((db as any).select({ count: sql<number>`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<any>((db as any).select({ count: sql<number>`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<any>((db as any).select({ count: sql<number>`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;