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
+31 -18
View File
@@ -6,9 +6,14 @@ import { eq, desc } from 'drizzle-orm';
import { requireAuth } from '../lib/auth.js';
import { generateId, getNow } from '../lib/utils.js';
import { emailService } from '../lib/email.js';
import { rateLimitMiddleware } from '../lib/rateLimit.js';
const contactsRouter = new Hono();
// Per-IP rate limit for public, unauthenticated write endpoints (contact form,
// newsletter subscribe/unsubscribe) to prevent spam and email flooding.
const publicFormLimit = rateLimitMiddleware({ max: 5, windowMs: 10 * 60 * 1000, prefix: 'contacts' });
// ==================== Sanitization Helpers ====================
/**
@@ -33,14 +38,14 @@ function sanitizeHeaderValue(str: string): string {
}
const createContactSchema = z.object({
name: z.string().min(2),
email: z.string().email(),
message: z.string().min(10),
name: z.string().min(2).max(200),
email: z.string().email().max(254),
message: z.string().min(10).max(5000),
});
const subscribeSchema = z.object({
email: z.string().email(),
name: z.string().optional(),
email: z.string().email().max(254),
name: z.string().max(200).optional(),
});
const updateContactSchema = z.object({
@@ -48,7 +53,7 @@ const updateContactSchema = z.object({
});
// Submit contact form (public)
contactsRouter.post('/', zValidator('json', createContactSchema), async (c) => {
contactsRouter.post('/', publicFormLimit, zValidator('json', createContactSchema), async (c) => {
const data = c.req.valid('json');
const now = getNow();
const id = generateId();
@@ -125,7 +130,7 @@ contactsRouter.post('/', zValidator('json', createContactSchema), async (c) => {
});
// Subscribe to newsletter (public)
contactsRouter.post('/subscribe', zValidator('json', subscribeSchema), async (c) => {
contactsRouter.post('/subscribe', publicFormLimit, zValidator('json', subscribeSchema), async (c) => {
const data = c.req.valid('json');
// Check if already subscribed
@@ -166,28 +171,30 @@ contactsRouter.post('/subscribe', zValidator('json', subscribeSchema), async (c)
});
// Unsubscribe from newsletter (public)
contactsRouter.post('/unsubscribe', zValidator('json', z.object({ email: z.string().email() })), async (c) => {
contactsRouter.post('/unsubscribe', publicFormLimit, zValidator('json', z.object({ email: z.string().email().max(254) })), async (c) => {
const { email } = c.req.valid('json');
const existing = await dbGet<any>(
(db as any).select().from(emailSubscribers).where(eq((emailSubscribers as any).email, email))
);
if (!existing) {
return c.json({ error: 'Email not found' }, 404);
// Always return the same response whether or not the address exists, to avoid
// leaking which emails are subscribed (enumeration).
if (existing && existing.status !== 'unsubscribed') {
await (db as any)
.update(emailSubscribers)
.set({ status: 'unsubscribed' })
.where(eq((emailSubscribers as any).id, existing.id));
}
await (db as any)
.update(emailSubscribers)
.set({ status: 'unsubscribed' })
.where(eq((emailSubscribers as any).id, existing.id));
return c.json({ message: 'Successfully unsubscribed' });
return c.json({ message: 'If this email was subscribed, it has been unsubscribed.' });
});
// Get all contacts (admin)
contactsRouter.get('/', requireAuth(['admin', 'organizer']), async (c) => {
const status = c.req.query('status');
const limit = Math.min(Math.max(parseInt(c.req.query('limit') || '100', 10) || 100, 1), 500);
const offset = Math.max(parseInt(c.req.query('offset') || '0', 10) || 0, 0);
let query = (db as any).select().from(contacts);
@@ -195,7 +202,9 @@ contactsRouter.get('/', requireAuth(['admin', 'organizer']), async (c) => {
query = query.where(eq((contacts as any).status, status));
}
const result = await dbAll(query.orderBy(desc((contacts as any).createdAt)));
const result = await dbAll(
query.orderBy(desc((contacts as any).createdAt)).limit(limit).offset(offset)
);
return c.json({ contacts: result });
});
@@ -255,6 +264,8 @@ contactsRouter.delete('/:id', requireAuth(['admin']), async (c) => {
// Get all subscribers (admin)
contactsRouter.get('/subscribers/list', requireAuth(['admin', 'marketing']), async (c) => {
const status = c.req.query('status');
const limit = Math.min(Math.max(parseInt(c.req.query('limit') || '100', 10) || 100, 1), 1000);
const offset = Math.max(parseInt(c.req.query('offset') || '0', 10) || 0, 0);
let query = (db as any).select().from(emailSubscribers);
@@ -262,7 +273,9 @@ contactsRouter.get('/subscribers/list', requireAuth(['admin', 'marketing']), asy
query = query.where(eq((emailSubscribers as any).status, status));
}
const result = await dbAll(query.orderBy(desc((emailSubscribers as any).createdAt)));
const result = await dbAll(
query.orderBy(desc((emailSubscribers as any).createdAt)).limit(limit).offset(offset)
);
return c.json({ subscribers: result });
});