Harden auth, payments, and frontend against review findings.

Close exploitable gaps in booking/payment flows, enforce token versioning and account checks, gate sensitive payment data, and add middleware plus input validation across admin routes.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
Michilis
2026-06-24 19:59:02 +00:00
co-authored by Cursor
parent fc4af38e8a
commit a6840ea953
37 changed files with 1432 additions and 528 deletions
+72 -27
View File
@@ -1,4 +1,6 @@
import { Hono } from 'hono';
import { zValidator } from '@hono/zod-validator';
import { z } from 'zod';
import { db, dbGet, dbAll, emailTemplates, emailLogs, events, tickets } from '../db/index.js';
import { eq, desc, and, or, sql } from 'drizzle-orm';
import { requireAuth } from '../lib/auth.js';
@@ -9,6 +11,50 @@ import { getQueueStatus } from '../lib/emailQueue.js';
const emailsRouter = new Hono();
const slugPattern = /^[a-z0-9-]+$/;
const createTemplateSchema = z.object({
name: z.string().min(1).max(255),
slug: z.string().min(1).max(100).regex(slugPattern),
subject: z.string().min(1).max(500),
subjectEs: z.string().max(500).optional().nullable(),
bodyHtml: z.string().min(1).max(200_000),
bodyHtmlEs: z.string().max(200_000).optional().nullable(),
bodyText: z.string().max(200_000).optional().nullable(),
bodyTextEs: z.string().max(200_000).optional().nullable(),
description: z.string().max(2000).optional().nullable(),
variables: z.array(z.any()).max(100).optional(),
});
const updateTemplateSchema = createTemplateSchema.partial().extend({
isActive: z.boolean().optional(),
});
const sendCustomEmailSchema = z.object({
to: z.string().email().max(254),
toName: z.string().max(200).optional(),
subject: z.string().min(1).max(500),
bodyHtml: z.string().min(1).max(200_000),
bodyText: z.string().max(200_000).optional(),
eventId: z.string().optional(),
});
const emailLogsQuerySchema = z.object({
limit: z.coerce.number().int().min(1).max(100).optional().default(50),
offset: z.coerce.number().int().min(0).optional().default(0),
});
// Safely parse a stored JSON variables column; a corrupt row must not 500 the route.
function safeParseVariables(raw: any): any[] {
if (!raw) return [];
try {
const parsed = JSON.parse(raw);
return Array.isArray(parsed) ? parsed : [];
} catch {
return [];
}
}
// ==================== Template Routes ====================
// Get all email templates
@@ -20,7 +66,7 @@ emailsRouter.get('/templates', requireAuth(['admin', 'organizer']), async (c) =>
// Parse variables JSON for each template
const parsedTemplates = templates.map((t: any) => ({
...t,
variables: t.variables ? JSON.parse(t.variables) : [],
variables: safeParseVariables(t.variables),
isSystem: Boolean(t.isSystem),
isActive: Boolean(t.isActive),
}));
@@ -46,7 +92,7 @@ emailsRouter.get('/templates/:id', requireAuth(['admin', 'organizer']), async (c
return c.json({
template: {
...template,
variables: template.variables ? JSON.parse(template.variables) : [],
variables: safeParseVariables(template.variables),
isSystem: Boolean(template.isSystem),
isActive: Boolean(template.isActive),
}
@@ -54,14 +100,10 @@ emailsRouter.get('/templates/:id', requireAuth(['admin', 'organizer']), async (c
});
// Create new email template
emailsRouter.post('/templates', requireAuth(['admin']), async (c) => {
const body = await c.req.json();
emailsRouter.post('/templates', requireAuth(['admin']), zValidator('json', createTemplateSchema), async (c) => {
const body = c.req.valid('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))
@@ -104,9 +146,9 @@ emailsRouter.post('/templates', requireAuth(['admin']), async (c) => {
});
// Update email template
emailsRouter.put('/templates/:id', requireAuth(['admin']), async (c) => {
emailsRouter.put('/templates/:id', requireAuth(['admin']), zValidator('json', updateTemplateSchema), async (c) => {
const { id } = c.req.param();
const body = await c.req.json();
const body = c.req.valid('json');
const existing = await dbGet<any>(
(db as any)
@@ -130,13 +172,14 @@ emailsRouter.put('/templates/:id', requireAuth(['admin']), async (c) => {
}
for (const field of allowedFields) {
if (body[field] !== undefined) {
const value = (body as Record<string, unknown>)[field];
if (value !== undefined) {
if (field === 'variables') {
updateData[field] = JSON.stringify(body[field]);
updateData[field] = JSON.stringify(value);
} else if (field === 'isActive') {
updateData[field] = body[field] ? 1 : 0;
updateData[field] = value ? 1 : 0;
} else {
updateData[field] = body[field];
updateData[field] = value;
}
}
}
@@ -156,7 +199,7 @@ emailsRouter.put('/templates/:id', requireAuth(['admin']), async (c) => {
return c.json({
template: {
...updated,
variables: updated.variables ? JSON.parse(updated.variables) : [],
variables: safeParseVariables(updated.variables),
isSystem: Boolean(updated.isSystem),
isActive: Boolean(updated.isActive),
},
@@ -203,16 +246,22 @@ emailsRouter.post('/send/event/:eventId', requireAuth(['admin', 'organizer']), a
const body = await c.req.json();
const { templateSlug, customVariables, recipientFilter } = body;
if (!templateSlug) {
if (!templateSlug || typeof templateSlug !== 'string') {
return c.json({ error: 'Template slug is required' }, 400);
}
const allowedFilters = ['confirmed', 'pending', 'all', 'checked_in'];
const filter = recipientFilter || 'confirmed';
if (!allowedFilters.includes(filter)) {
return c.json({ error: `Invalid recipientFilter. Allowed: ${allowedFilters.join(', ')}` }, 400);
}
// Queue emails for background processing instead of sending synchronously
const result = await emailService.queueEventEmails({
eventId,
templateSlug,
customVariables,
recipientFilter: recipientFilter || 'confirmed',
recipientFilter: filter,
sentBy: user?.id,
});
@@ -220,14 +269,9 @@ emailsRouter.post('/send/event/:eventId', requireAuth(['admin', 'organizer']), a
});
// Send custom email to specific recipients
emailsRouter.post('/send/custom', requireAuth(['admin', 'organizer']), async (c) => {
emailsRouter.post('/send/custom', requireAuth(['admin', 'organizer']), zValidator('json', sendCustomEmailSchema), 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 { to, toName, subject, bodyHtml, bodyText, eventId } = c.req.valid('json');
const result = await emailService.sendCustomEmail({
to,
@@ -272,7 +316,7 @@ emailsRouter.post('/preview', requireAuth(['admin', 'organizer']), async (c) =>
: template.bodyHtml;
const finalSubject = replaceTemplateVariables(subject, allVariables);
const finalBodyContent = replaceTemplateVariables(bodyHtml, allVariables);
const finalBodyContent = replaceTemplateVariables(bodyHtml, allVariables, true);
const finalBodyHtml = wrapInBaseTemplate(finalBodyContent, { ...allVariables, subject: finalSubject });
return c.json({
@@ -288,8 +332,9 @@ emailsRouter.get('/logs', requireAuth(['admin', 'organizer']), async (c) => {
const eventId = c.req.query('eventId');
const status = c.req.query('status');
const search = c.req.query('search');
const limit = parseInt(c.req.query('limit') || '50');
const offset = parseInt(c.req.query('offset') || '0');
// Clamp pagination so a NaN / out-of-range value can't produce undefined query behaviour.
const limit = Math.min(Math.max(parseInt(c.req.query('limit') || '50', 10) || 50, 1), 200);
const offset = Math.max(parseInt(c.req.query('offset') || '0', 10) || 0, 0);
let query = (db as any).select().from(emailLogs);