import { Hono } from 'hono'; import { zValidator } from '@hono/zod-validator'; import { z } from 'zod'; import { db, dbGet, dbAll, users, tickets, payments, events, invoices, User } from '../db/index.js'; import { eq, desc, and, gt, sql } from 'drizzle-orm'; import { requireAuth, getUserSessions, invalidateSession, invalidateAllUserSessions, hashPassword, validatePassword } from '../lib/auth.js'; import { generateId, getNow } from '../lib/utils.js'; // User type that includes all fields (some added in schema updates) type AuthUser = User & { isClaimed: boolean; googleId: string | null; rucNumber: string | null; accountStatus: string; }; const dashboard = new Hono(); // Apply authentication to all routes dashboard.use('*', requireAuth()); // ==================== Profile Routes ==================== const updateProfileSchema = z.object({ name: z.string().min(2).optional(), phone: z.string().optional(), languagePreference: z.enum(['en', 'es']).optional(), rucNumber: z.string().max(15).optional(), }); // Get user profile dashboard.get('/profile', async (c) => { const user = (c as any).get('user') as AuthUser; // Get membership duration const createdDate = new Date(user.createdAt); const now = new Date(); const membershipDays = Math.floor((now.getTime() - createdDate.getTime()) / (1000 * 60 * 60 * 24)); return c.json({ profile: { id: user.id, email: user.email, name: user.name, phone: user.phone, languagePreference: user.languagePreference, rucNumber: user.rucNumber, isClaimed: user.isClaimed, accountStatus: user.accountStatus, hasPassword: !!user.password, hasGoogleLinked: !!user.googleId, memberSince: user.createdAt, membershipDays, createdAt: user.createdAt, }, }); }); // Update profile dashboard.put('/profile', zValidator('json', updateProfileSchema), async (c) => { const user = (c as any).get('user') as AuthUser; const data = c.req.valid('json'); const now = getNow(); await (db as any) .update(users) .set({ ...data, updatedAt: now, }) .where(eq((users as any).id, user.id)); const updatedUser = await dbGet( (db as any) .select() .from(users) .where(eq((users as any).id, user.id)) ); return c.json({ profile: { id: updatedUser.id, email: updatedUser.email, name: updatedUser.name, phone: updatedUser.phone, languagePreference: updatedUser.languagePreference, rucNumber: updatedUser.rucNumber, }, message: 'Profile updated successfully', }); }); // ==================== Tickets Routes ==================== // Get user's tickets dashboard.get('/tickets', async (c) => { const user = (c as any).get('user') as AuthUser; const userTickets = await dbAll( (db as any) .select() .from(tickets) .where(eq((tickets as any).userId, user.id)) .orderBy(desc((tickets as any).createdAt)) ); // Get event details for each ticket const ticketsWithEvents = await Promise.all( userTickets.map(async (ticket: any) => { const event = await dbGet( (db as any) .select() .from(events) .where(eq((events as any).id, ticket.eventId)) ); const payment = await dbGet( (db as any) .select() .from(payments) .where(eq((payments as any).ticketId, ticket.id)) ); // Check for invoice let invoice: any = null; if (payment && payment.status === 'paid') { invoice = await dbGet( (db as any) .select() .from(invoices) .where(eq((invoices as any).paymentId, payment.id)) ); } return { ...ticket, event: event ? { id: event.id, title: event.title, titleEs: event.titleEs, startDatetime: event.startDatetime, endDatetime: event.endDatetime, location: event.location, locationUrl: event.locationUrl, price: event.price, currency: event.currency, status: event.status, bannerUrl: event.bannerUrl, } : null, payment: payment ? { id: payment.id, provider: payment.provider, amount: payment.amount, currency: payment.currency, status: payment.status, paidAt: payment.paidAt, } : null, invoice: invoice ? { id: invoice.id, invoiceNumber: invoice.invoiceNumber, pdfUrl: invoice.pdfUrl, createdAt: invoice.createdAt, } : null, }; }) ); return c.json({ tickets: ticketsWithEvents }); }); // Get single ticket detail dashboard.get('/tickets/:id', async (c) => { const user = (c as any).get('user') as AuthUser; const ticketId = c.req.param('id'); const ticket = await dbGet( (db as any) .select() .from(tickets) .where( and( eq((tickets as any).id, ticketId), eq((tickets as any).userId, user.id) ) ) ); if (!ticket) { return c.json({ error: 'Ticket not found' }, 404); } const event = await dbGet( (db as any) .select() .from(events) .where(eq((events as any).id, ticket.eventId)) ); const payment = await dbGet( (db as any) .select() .from(payments) .where(eq((payments as any).ticketId, ticket.id)) ); let invoice = null; if (payment && payment.status === 'paid') { invoice = await dbGet( (db as any) .select() .from(invoices) .where(eq((invoices as any).paymentId, payment.id)) ); } return c.json({ ticket: { ...ticket, event, payment, invoice, }, }); }); // ==================== Next Event Route ==================== // Get next upcoming event for user dashboard.get('/next-event', async (c) => { const user = (c as any).get('user') as AuthUser; const now = getNow(); // Get user's tickets for upcoming events const userTickets = await dbAll( (db as any) .select() .from(tickets) .where(eq((tickets as any).userId, user.id)) ); if (userTickets.length === 0) { return c.json({ nextEvent: null }); } // Find the next upcoming event let nextEvent = null; let nextTicket = null; let nextPayment = null; for (const ticket of userTickets) { if (ticket.status === 'cancelled') continue; const event = await dbGet( (db as any) .select() .from(events) .where(eq((events as any).id, ticket.eventId)) ); if (!event) continue; // Check if event is in the future if (new Date(event.startDatetime) > new Date()) { if (!nextEvent || new Date(event.startDatetime) < new Date(nextEvent.startDatetime)) { nextEvent = event; nextTicket = ticket; nextPayment = await dbGet( (db as any) .select() .from(payments) .where(eq((payments as any).ticketId, ticket.id)) ); } } } if (!nextEvent) { return c.json({ nextEvent: null }); } return c.json({ nextEvent: { event: nextEvent, ticket: nextTicket, payment: nextPayment, }, }); }); // ==================== Payments & Invoices Routes ==================== // Get payment history dashboard.get('/payments', async (c) => { const user = (c as any).get('user') as AuthUser; // Get all user's tickets first const userTickets = await dbAll( (db as any) .select() .from(tickets) .where(eq((tickets as any).userId, user.id)) ); const ticketIds = userTickets.map((t: any) => t.id); if (ticketIds.length === 0) { return c.json({ payments: [] }); } // Get all payments for user's tickets const allPayments = []; for (const ticketId of ticketIds) { const ticketPayments = await dbAll( (db as any) .select() .from(payments) .where(eq((payments as any).ticketId, ticketId)) ); for (const payment of ticketPayments) { const ticket = userTickets.find((t: any) => t.id === payment.ticketId); const event = ticket ? await dbGet( (db as any) .select() .from(events) .where(eq((events as any).id, ticket.eventId)) ) : null; let invoice: any = null; if (payment.status === 'paid') { invoice = await dbGet( (db as any) .select() .from(invoices) .where(eq((invoices as any).paymentId, payment.id)) ); } allPayments.push({ ...payment, ticket: ticket ? { id: ticket.id, attendeeFirstName: ticket.attendeeFirstName, attendeeLastName: ticket.attendeeLastName, status: ticket.status, } : null, event: event ? { id: event.id, title: event.title, titleEs: event.titleEs, startDatetime: event.startDatetime, } : null, invoice: invoice ? { id: invoice.id, invoiceNumber: invoice.invoiceNumber, pdfUrl: invoice.pdfUrl, } : null, }); } } // Sort by createdAt desc allPayments.sort((a, b) => new Date(b.createdAt).getTime() - new Date(a.createdAt).getTime()); return c.json({ payments: allPayments }); }); // Get invoices dashboard.get('/invoices', async (c) => { const user = (c as any).get('user') as AuthUser; const userInvoices = await dbAll( (db as any) .select() .from(invoices) .where(eq((invoices as any).userId, user.id)) .orderBy(desc((invoices as any).createdAt)) ); // Get payment and event details for each invoice const invoicesWithDetails = await Promise.all( userInvoices.map(async (invoice: any) => { const payment = await dbGet( (db as any) .select() .from(payments) .where(eq((payments as any).id, invoice.paymentId)) ); let event: any = null; if (payment) { const ticket = await dbGet( (db as any) .select() .from(tickets) .where(eq((tickets as any).id, payment.ticketId)) ); if (ticket) { event = await dbGet( (db as any) .select() .from(events) .where(eq((events as any).id, ticket.eventId)) ); } } return { ...invoice, event: event ? { id: event.id, title: event.title, titleEs: event.titleEs, startDatetime: event.startDatetime, } : null, }; }) ); return c.json({ invoices: invoicesWithDetails }); }); // ==================== Security Routes ==================== // Get active sessions dashboard.get('/sessions', async (c) => { const user = (c as any).get('user') as AuthUser; const sessions = await getUserSessions(user.id); return c.json({ sessions: sessions.map((s: any) => ({ id: s.id, userAgent: s.userAgent, ipAddress: s.ipAddress, lastActiveAt: s.lastActiveAt, createdAt: s.createdAt, })), }); }); // Revoke a specific session dashboard.delete('/sessions/:id', async (c) => { const user = (c as any).get('user') as AuthUser; const sessionId = c.req.param('id'); await invalidateSession(sessionId, user.id); return c.json({ message: 'Session revoked' }); }); // Revoke all sessions (logout everywhere) dashboard.post('/sessions/revoke-all', async (c) => { const user = (c as any).get('user') as AuthUser; await invalidateAllUserSessions(user.id); return c.json({ message: 'All sessions revoked. Please log in again.' }); }); // Set password (for users without one) const setPasswordSchema = z.object({ password: z.string().min(10, 'Password must be at least 10 characters'), }); dashboard.post('/set-password', zValidator('json', setPasswordSchema), async (c) => { const user = (c as any).get('user') as AuthUser; const { password } = c.req.valid('json'); // Check if user already has a password if (user.password) { return c.json({ error: 'Password already set. Use change password instead.' }, 400); } const passwordValidation = validatePassword(password); if (!passwordValidation.valid) { return c.json({ error: passwordValidation.error }, 400); } const hashedPassword = await hashPassword(password); const now = getNow(); await (db as any) .update(users) .set({ password: hashedPassword, updatedAt: now, }) .where(eq((users as any).id, user.id)); return c.json({ message: 'Password set successfully' }); }); // Unlink Google account (only if password is set) dashboard.post('/unlink-google', async (c) => { const user = (c as any).get('user') as AuthUser; if (!user.googleId) { return c.json({ error: 'Google account not linked' }, 400); } if (!user.password) { return c.json({ error: 'Cannot unlink Google without a password set' }, 400); } const now = getNow(); await (db as any) .update(users) .set({ googleId: null, updatedAt: now, }) .where(eq((users as any).id, user.id)); return c.json({ message: 'Google account unlinked' }); }); // ==================== Dashboard Summary Route ==================== // Get dashboard summary (welcome panel data) dashboard.get('/summary', async (c) => { const user = (c as any).get('user') as AuthUser; const now = new Date(); // Get membership duration const createdDate = new Date(user.createdAt); const membershipDays = Math.floor((now.getTime() - createdDate.getTime()) / (1000 * 60 * 60 * 24)); // Get ticket count const userTickets = await dbAll( (db as any) .select() .from(tickets) .where(eq((tickets as any).userId, user.id)) ); const totalTickets = userTickets.length; const confirmedTickets = userTickets.filter((t: any) => t.status === 'confirmed' || t.status === 'checked_in').length; const upcomingTickets = []; for (const ticket of userTickets) { if (ticket.status === 'cancelled') continue; const event = await dbGet( (db as any) .select() .from(events) .where(eq((events as any).id, ticket.eventId)) ); if (event && new Date(event.startDatetime) > now) { upcomingTickets.push({ ticket, event }); } } // Get pending payments count const ticketIds = userTickets.map((t: any) => t.id); let pendingPayments = 0; for (const ticketId of ticketIds) { const payment = await dbGet( (db as any) .select() .from(payments) .where( and( eq((payments as any).ticketId, ticketId), eq((payments as any).status, 'pending_approval') ) ) ); if (payment) pendingPayments++; } return c.json({ summary: { user: { name: user.name, email: user.email, accountStatus: user.accountStatus, memberSince: user.createdAt, membershipDays, }, stats: { totalTickets, confirmedTickets, upcomingEvents: upcomingTickets.length, pendingPayments, }, }, }); }); export default dashboard;