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
+66 -76
View File
@@ -67,14 +67,14 @@ adminRouter.get('/dashboard', requireAuth(['admin', 'organizer']), async (c) =>
.where(eq((payments as any).status, 'pending'))
);
const paidPayments = await dbAll<any>(
const revenueRow = await dbGet<any>(
(db as any)
.select()
.select({ total: sql<number>`COALESCE(SUM(${(payments as any).amount}), 0)` })
.from(payments)
.where(eq((payments as any).status, 'paid'))
);
const totalRevenue = paidPayments.reduce((sum: number, p: any) => sum + Number(p.amount || 0), 0);
const totalRevenue = Number(revenueRow?.total || 0);
const newContacts = await dbGet<any>(
(db as any)
@@ -110,56 +110,52 @@ adminRouter.get('/dashboard', requireAuth(['admin', 'organizer']), async (c) =>
// Get analytics data (admin)
adminRouter.get('/analytics', requireAuth(['admin']), async (c) => {
// Get events with ticket counts
// Get events with ticket counts using grouped aggregates (avoids 3 queries per event).
const allEvents = await dbAll<any>((db as any).select().from(events));
const eventStats = await Promise.all(
allEvents.map(async (event: any) => {
const ticketCount = await dbGet<any>(
(db as any)
.select({ count: sql<number>`count(*)` })
.from(tickets)
.where(eq((tickets as any).eventId, event.id))
);
const confirmedCount = await dbGet<any>(
(db as any)
.select({ count: sql<number>`count(*)` })
.from(tickets)
.where(
and(
eq((tickets as any).eventId, event.id),
eq((tickets as any).status, 'confirmed'),
ne((tickets as any).isGuest, 1)
)
)
);
const checkedInCount = await dbGet<any>(
(db as any)
.select({ count: sql<number>`count(*)` })
.from(tickets)
.where(
and(
eq((tickets as any).eventId, event.id),
eq((tickets as any).status, 'checked_in'),
ne((tickets as any).isGuest, 1)
)
)
);
return {
id: event.id,
title: event.title,
date: event.startDatetime,
capacity: event.capacity,
totalBookings: ticketCount?.count || 0,
confirmedBookings: confirmedCount?.count || 0,
checkedIn: checkedInCount?.count || 0,
revenue: (confirmedCount?.count || 0) * event.price,
};
})
const totalRows = await dbAll<any>(
(db as any)
.select({ eventId: (tickets as any).eventId, count: sql<number>`count(*)` })
.from(tickets)
.groupBy((tickets as any).eventId)
);
const confirmedRows = await dbAll<any>(
(db as any)
.select({ eventId: (tickets as any).eventId, count: sql<number>`count(*)` })
.from(tickets)
.where(and(eq((tickets as any).status, 'confirmed'), ne((tickets as any).isGuest, 1)))
.groupBy((tickets as any).eventId)
);
const checkedInRows = await dbAll<any>(
(db as any)
.select({ eventId: (tickets as any).eventId, count: sql<number>`count(*)` })
.from(tickets)
.where(and(eq((tickets as any).status, 'checked_in'), ne((tickets as any).isGuest, 1)))
.groupBy((tickets as any).eventId)
);
const toMap = (rows: any[]) => {
const m = new Map<string, number>();
for (const r of rows) m.set(r.eventId, Number(r.count) || 0);
return m;
};
const totalMap = toMap(totalRows);
const confirmedMap = toMap(confirmedRows);
const checkedInMap = toMap(checkedInRows);
const eventStats = allEvents.map((event: any) => {
const confirmedBookings = confirmedMap.get(event.id) || 0;
return {
id: event.id,
title: event.title,
date: event.startDatetime,
capacity: event.capacity,
totalBookings: totalMap.get(event.id) || 0,
confirmedBookings,
checkedIn: checkedInMap.get(event.id) || 0,
revenue: confirmedBookings * event.price,
};
});
return c.json({
analytics: {
@@ -179,30 +175,25 @@ adminRouter.get('/export/tickets', requireAuth(['admin']), async (c) => {
}
const ticketList = await dbAll<any>(query);
const userIds = [...new Set(ticketList.map((t: any) => t.userId).filter(Boolean))];
const eventIds = [...new Set(ticketList.map((t: any) => t.eventId).filter(Boolean))];
const ticketIds = ticketList.map((t: any) => t.id);
const [userRows, eventRows, paymentRows] = await Promise.all([
userIds.length ? dbAll<any>((db as any).select().from(users).where(inArray((users as any).id, userIds))) : Promise.resolve([]),
eventIds.length ? dbAll<any>((db as any).select().from(events).where(inArray((events as any).id, eventIds))) : Promise.resolve([]),
ticketIds.length ? dbAll<any>((db as any).select().from(payments).where(inArray((payments as any).ticketId, ticketIds))) : Promise.resolve([]),
]);
const usersById = new Map(userRows.map((u: any) => [u.id, u]));
const eventsById = new Map(eventRows.map((e: any) => [e.id, e]));
const paymentsByTicketId = new Map(paymentRows.map((p: any) => [p.ticketId, p]));
// Get user and event details for each ticket
const enrichedTickets = await Promise.all(
ticketList.map(async (ticket: any) => {
const user = await dbGet<any>(
(db as any)
.select()
.from(users)
.where(eq((users as any).id, ticket.userId))
);
const event = await dbGet<any>(
(db as any)
.select()
.from(events)
.where(eq((events as any).id, ticket.eventId))
);
const payment = await dbGet<any>(
(db as any)
.select()
.from(payments)
.where(eq((payments as any).ticketId, ticket.id))
);
const enrichedTickets = ticketList.map((ticket: any) => {
const user = usersById.get(ticket.userId);
const event = eventsById.get(ticket.eventId);
const payment = paymentsByTicketId.get(ticket.id);
return {
ticketId: ticket.id,
@@ -218,8 +209,7 @@ adminRouter.get('/export/tickets', requireAuth(['admin']), async (c) => {
paymentAmount: payment?.amount,
createdAt: ticket.createdAt,
};
})
);
});
return c.json({ tickets: enrichedTickets });
});