import { Hono } from 'hono'; import { zValidator } from '@hono/zod-validator'; import { z } from 'zod'; import { db, dbGet, dbAll, users, tickets, events, payments, magicLinkTokens, userSessions, invoices, auditLogs, emailLogs, paymentOptions, legalPages, siteSettings } from '../db/index.js'; import { eq, desc, sql } from 'drizzle-orm'; import { requireAuth } from '../lib/auth.js'; import { getNow } from '../lib/utils.js'; interface UserContext { id: string; email: string; name: string; role: string; } const usersRouter = new Hono<{ Variables: { user: UserContext } }>(); const updateUserSchema = z.object({ name: z.string().min(2).optional(), email: z.string().email().optional(), phone: z.string().optional(), role: z.enum(['admin', 'organizer', 'staff', 'marketing', 'user']).optional(), languagePreference: z.enum(['en', 'es']).optional(), accountStatus: z.enum(['active', 'unclaimed', 'suspended']).optional(), }); // Get all users (admin only) usersRouter.get('/', requireAuth(['admin']), async (c) => { const role = c.req.query('role'); let query = (db as any).select({ id: (users as any).id, email: (users as any).email, name: (users as any).name, phone: (users as any).phone, role: (users as any).role, languagePreference: (users as any).languagePreference, isClaimed: (users as any).isClaimed, rucNumber: (users as any).rucNumber, accountStatus: (users as any).accountStatus, createdAt: (users as any).createdAt, }).from(users); if (role) { query = query.where(eq((users as any).role, role)); } const result = await dbAll(query.orderBy(desc((users as any).createdAt))); return c.json({ users: result }); }); // Get user by ID (admin or self) usersRouter.get('/:id', requireAuth(['admin', 'organizer', 'staff', 'marketing', 'user']), async (c) => { const id = c.req.param('id'); const currentUser = c.get('user'); // Users can only view their own profile unless admin if (currentUser.role !== 'admin' && currentUser.id !== id) { return c.json({ error: 'Forbidden' }, 403); } const user = await dbGet( (db as any) .select({ id: (users as any).id, email: (users as any).email, name: (users as any).name, phone: (users as any).phone, role: (users as any).role, languagePreference: (users as any).languagePreference, isClaimed: (users as any).isClaimed, rucNumber: (users as any).rucNumber, accountStatus: (users as any).accountStatus, createdAt: (users as any).createdAt, }) .from(users) .where(eq((users as any).id, id)) ); if (!user) { return c.json({ error: 'User not found' }, 404); } return c.json({ user }); }); // Update user (admin or self) usersRouter.put('/:id', requireAuth(['admin', 'organizer', 'staff', 'marketing', 'user']), zValidator('json', updateUserSchema), async (c) => { const id = c.req.param('id'); const data = c.req.valid('json'); const currentUser = c.get('user'); // Users can only update their own profile unless admin if (currentUser.role !== 'admin' && currentUser.id !== id) { return c.json({ error: 'Forbidden' }, 403); } // Only admin can change roles, email, and account status if (data.role && currentUser.role !== 'admin') { delete data.role; } if (data.email && currentUser.role !== 'admin') { delete data.email; } if (data.accountStatus && currentUser.role !== 'admin') { delete data.accountStatus; } const existing = await dbGet( (db as any).select().from(users).where(eq((users as any).id, id)) ); if (!existing) { return c.json({ error: 'User not found' }, 404); } await (db as any) .update(users) .set({ ...data, updatedAt: getNow() }) .where(eq((users as any).id, id)); const updated = await dbGet( (db as any) .select({ id: (users as any).id, email: (users as any).email, name: (users as any).name, phone: (users as any).phone, role: (users as any).role, languagePreference: (users as any).languagePreference, isClaimed: (users as any).isClaimed, rucNumber: (users as any).rucNumber, accountStatus: (users as any).accountStatus, createdAt: (users as any).createdAt, }) .from(users) .where(eq((users as any).id, id)) ); return c.json({ user: updated }); }); // Get user's ticket history usersRouter.get('/:id/history', requireAuth(['admin', 'organizer', 'staff', 'marketing', 'user']), async (c) => { const id = c.req.param('id'); const currentUser = c.get('user'); // Users can only view their own history unless admin/organizer if (!['admin', 'organizer'].includes(currentUser.role) && currentUser.id !== id) { return c.json({ error: 'Forbidden' }, 403); } const userTickets = await dbAll( (db as any) .select() .from(tickets) .where(eq((tickets as any).userId, id)) .orderBy(desc((tickets as any).createdAt)) ); // Get event details for each ticket const history = 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)) ); return { ...ticket, event, }; }) ); return c.json({ history }); }); // Delete user (admin only) usersRouter.delete('/:id', requireAuth(['admin']), async (c) => { const id = c.req.param('id'); const currentUser = c.get('user'); // Prevent self-deletion if (currentUser.id === id) { return c.json({ error: 'Cannot delete your own account' }, 400); } const existing = await dbGet( (db as any).select().from(users).where(eq((users as any).id, id)) ); if (!existing) { return c.json({ error: 'User not found' }, 404); } // Prevent deleting admin users if (existing.role === 'admin') { return c.json({ error: 'Cannot delete admin users' }, 400); } try { // Get all tickets for this user const userTickets = await dbAll( (db as any) .select() .from(tickets) .where(eq((tickets as any).userId, id)) ); // Delete invoices associated with user's tickets (invoices reference payments which reference tickets) for (const ticket of userTickets) { // Get payments for this ticket const ticketPayments = await dbAll( (db as any) .select() .from(payments) .where(eq((payments as any).ticketId, ticket.id)) ); // Delete invoices for each payment for (const payment of ticketPayments) { await (db as any).delete(invoices).where(eq((invoices as any).paymentId, payment.id)); } // Delete payments for this ticket await (db as any).delete(payments).where(eq((payments as any).ticketId, ticket.id)); } // Delete invoices directly associated with the user (if any) await (db as any).delete(invoices).where(eq((invoices as any).userId, id)); // Delete user's tickets await (db as any).delete(tickets).where(eq((tickets as any).userId, id)); // Delete magic link tokens for the user await (db as any).delete(magicLinkTokens).where(eq((magicLinkTokens as any).userId, id)); // Delete user sessions await (db as any).delete(userSessions).where(eq((userSessions as any).userId, id)); // Set userId to null in audit_logs (nullable reference) await (db as any) .update(auditLogs) .set({ userId: null }) .where(eq((auditLogs as any).userId, id)); // Set sentBy to null in email_logs (nullable reference) await (db as any) .update(emailLogs) .set({ sentBy: null }) .where(eq((emailLogs as any).sentBy, id)); // Set updatedBy to null in payment_options (nullable reference) await (db as any) .update(paymentOptions) .set({ updatedBy: null }) .where(eq((paymentOptions as any).updatedBy, id)); // Set updatedBy to null in legal_pages (nullable reference) await (db as any) .update(legalPages) .set({ updatedBy: null }) .where(eq((legalPages as any).updatedBy, id)); // Set updatedBy to null in site_settings (nullable reference) await (db as any) .update(siteSettings) .set({ updatedBy: null }) .where(eq((siteSettings as any).updatedBy, id)); // Clear checkedInByAdminId references in tickets await (db as any) .update(tickets) .set({ checkedInByAdminId: null }) .where(eq((tickets as any).checkedInByAdminId, id)); // Delete the user await (db as any).delete(users).where(eq((users as any).id, id)); return c.json({ message: 'User deleted successfully' }); } catch (error) { console.error('Error deleting user:', error); return c.json({ error: 'Failed to delete user. They may have related records.' }, 500); } }); // Get user statistics (admin) usersRouter.get('/stats/overview', requireAuth(['admin']), async (c) => { const totalUsers = await dbGet( (db as any) .select({ count: sql`count(*)` }) .from(users) ); const adminCount = await dbGet( (db as any) .select({ count: sql`count(*)` }) .from(users) .where(eq((users as any).role, 'admin')) ); return c.json({ stats: { total: totalUsers?.count || 0, admins: adminCount?.count || 0, }, }); }); export default usersRouter;