import 'dotenv/config'; import { serve } from '@hono/node-server'; import { Hono } from 'hono'; import { cors } from 'hono/cors'; import { logger } from 'hono/logger'; import { swaggerUI } from '@hono/swagger-ui'; import { serveStatic } from '@hono/node-server/serve-static'; import authRoutes from './routes/auth.js'; import eventsRoutes from './routes/events.js'; import ticketsRoutes from './routes/tickets.js'; import usersRoutes from './routes/users.js'; import contactsRoutes from './routes/contacts.js'; import paymentsRoutes from './routes/payments.js'; import adminRoutes from './routes/admin.js'; import mediaRoutes from './routes/media.js'; import lnbitsRoutes from './routes/lnbits.js'; import emailsRoutes from './routes/emails.js'; import paymentOptionsRoutes from './routes/payment-options.js'; import dashboardRoutes from './routes/dashboard.js'; import emailService from './lib/email.js'; const app = new Hono(); // Middleware app.use('*', logger()); // CORS: Only enable in development. In production, nginx handles CORS. if (process.env.NODE_ENV !== 'production') { app.use('*', cors({ origin: process.env.FRONTEND_URL || 'http://localhost:3002', credentials: true, })); } // OpenAPI specification const openApiSpec = { openapi: '3.0.0', info: { title: 'Spanglish API', version: '2.0.0', description: 'API for Spanglish Language Exchange Event Platform - includes authentication, user dashboard, event management, tickets, payments, and more.', contact: { name: 'Spanglish', url: 'https://spanglish.com', }, }, servers: [ { url: process.env.API_URL || 'http://localhost:3001', description: 'API Server', }, ], tags: [ { name: 'Auth', description: 'Authentication and account management' }, { name: 'User Dashboard', description: 'User dashboard and profile endpoints' }, { name: 'Events', description: 'Event management' }, { name: 'Tickets', description: 'Ticket booking and management' }, { name: 'Payments', description: 'Payment management' }, { name: 'Payment Options', description: 'Payment configuration' }, { name: 'Users', description: 'User management (admin)' }, { name: 'Contacts', description: 'Contact and subscription management' }, { name: 'Emails', description: 'Email templates and sending' }, { name: 'Media', description: 'File uploads and media management' }, { name: 'Lightning', description: 'Lightning/Bitcoin payments via LNBits' }, { name: 'Admin', description: 'Admin dashboard and analytics' }, ], paths: { // ==================== Auth Endpoints ==================== '/api/auth/register': { post: { tags: ['Auth'], summary: 'Register a new user', description: 'Create a new user account. First registered user becomes admin. Password must be at least 10 characters.', requestBody: { required: true, content: { 'application/json': { schema: { type: 'object', required: ['email', 'password', 'name'], properties: { email: { type: 'string', format: 'email' }, password: { type: 'string', minLength: 10, description: 'Minimum 10 characters' }, name: { type: 'string', minLength: 2 }, phone: { type: 'string' }, languagePreference: { type: 'string', enum: ['en', 'es'] }, }, }, }, }, }, responses: { 201: { description: 'User created successfully' }, 400: { description: 'Email already registered or validation error' }, }, }, }, '/api/auth/login': { post: { tags: ['Auth'], summary: 'Login with email and password', description: 'Authenticate user with email and password. Rate limited to 5 attempts per 15 minutes.', requestBody: { required: true, content: { 'application/json': { schema: { type: 'object', required: ['email', 'password'], properties: { email: { type: 'string', format: 'email' }, password: { type: 'string' }, }, }, }, }, }, responses: { 200: { description: 'Login successful, returns JWT token' }, 401: { description: 'Invalid credentials' }, 429: { description: 'Too many login attempts' }, }, }, }, '/api/auth/google': { post: { tags: ['Auth'], summary: 'Login or register with Google', description: 'Authenticate using Google OAuth. Creates account if user does not exist.', requestBody: { required: true, content: { 'application/json': { schema: { type: 'object', required: ['credential'], properties: { credential: { type: 'string', description: 'Google ID token' }, }, }, }, }, }, responses: { 200: { description: 'Login successful' }, 400: { description: 'Invalid Google token' }, }, }, }, '/api/auth/magic-link/request': { post: { tags: ['Auth'], summary: 'Request magic link login', description: 'Send a one-time login link to email. Link expires in 10 minutes.', requestBody: { required: true, content: { 'application/json': { schema: { type: 'object', required: ['email'], properties: { email: { type: 'string', format: 'email' }, }, }, }, }, }, responses: { 200: { description: 'Magic link sent (if account exists)' }, }, }, }, '/api/auth/magic-link/verify': { post: { tags: ['Auth'], summary: 'Verify magic link token', description: 'Verify the magic link token and login user.', requestBody: { required: true, content: { 'application/json': { schema: { type: 'object', required: ['token'], properties: { token: { type: 'string' }, }, }, }, }, }, responses: { 200: { description: 'Login successful' }, 400: { description: 'Invalid or expired token' }, }, }, }, '/api/auth/password-reset/request': { post: { tags: ['Auth'], summary: 'Request password reset', description: 'Send a password reset link to email. Link expires in 30 minutes.', requestBody: { required: true, content: { 'application/json': { schema: { type: 'object', required: ['email'], properties: { email: { type: 'string', format: 'email' }, }, }, }, }, }, responses: { 200: { description: 'Reset link sent (if account exists)' }, }, }, }, '/api/auth/password-reset/confirm': { post: { tags: ['Auth'], summary: 'Confirm password reset', description: 'Reset password using the token from email.', requestBody: { required: true, content: { 'application/json': { schema: { type: 'object', required: ['token', 'password'], properties: { token: { type: 'string' }, password: { type: 'string', minLength: 10 }, }, }, }, }, }, responses: { 200: { description: 'Password reset successful' }, 400: { description: 'Invalid or expired token' }, }, }, }, '/api/auth/claim-account/request': { post: { tags: ['Auth'], summary: 'Request account claim link', description: 'For unclaimed accounts created during booking. Link expires in 24 hours.', requestBody: { required: true, content: { 'application/json': { schema: { type: 'object', required: ['email'], properties: { email: { type: 'string', format: 'email' }, }, }, }, }, }, responses: { 200: { description: 'Claim link sent (if unclaimed account exists)' }, }, }, }, '/api/auth/claim-account/confirm': { post: { tags: ['Auth'], summary: 'Confirm account claim', description: 'Claim an unclaimed account by setting password or linking Google.', requestBody: { required: true, content: { 'application/json': { schema: { type: 'object', required: ['token'], properties: { token: { type: 'string' }, password: { type: 'string', minLength: 10, description: 'Required if not linking Google' }, googleId: { type: 'string', description: 'Google ID for OAuth linking' }, }, }, }, }, }, responses: { 200: { description: 'Account claimed successfully' }, 400: { description: 'Invalid token or missing credentials' }, }, }, }, '/api/auth/change-password': { post: { tags: ['Auth'], summary: 'Change password', description: 'Change password for authenticated user.', security: [{ bearerAuth: [] }], requestBody: { required: true, content: { 'application/json': { schema: { type: 'object', required: ['currentPassword', 'newPassword'], properties: { currentPassword: { type: 'string' }, newPassword: { type: 'string', minLength: 10 }, }, }, }, }, }, responses: { 200: { description: 'Password changed' }, 400: { description: 'Current password incorrect' }, 401: { description: 'Unauthorized' }, }, }, }, '/api/auth/me': { get: { tags: ['Auth'], summary: 'Get current user', description: 'Get the currently authenticated user profile.', security: [{ bearerAuth: [] }], responses: { 200: { description: 'Current user data' }, 401: { description: 'Unauthorized' }, }, }, }, '/api/auth/logout': { post: { tags: ['Auth'], summary: 'Logout', description: 'Logout current user (client-side token removal).', responses: { 200: { description: 'Logged out' }, }, }, }, // ==================== User Dashboard Endpoints ==================== '/api/dashboard/summary': { get: { tags: ['User Dashboard'], summary: 'Get dashboard summary', description: 'Get user stats including ticket counts, membership duration, etc.', security: [{ bearerAuth: [] }], responses: { 200: { description: 'Dashboard summary data' }, 401: { description: 'Unauthorized' }, }, }, }, '/api/dashboard/profile': { get: { tags: ['User Dashboard'], summary: 'Get user profile', description: 'Get detailed user profile information.', security: [{ bearerAuth: [] }], responses: { 200: { description: 'User profile' }, 401: { description: 'Unauthorized' }, }, }, put: { tags: ['User Dashboard'], summary: 'Update user profile', description: 'Update user profile fields like name, phone, language preference, RUC number.', security: [{ bearerAuth: [] }], requestBody: { required: true, content: { 'application/json': { schema: { type: 'object', properties: { name: { type: 'string', minLength: 2 }, phone: { type: 'string' }, languagePreference: { type: 'string', enum: ['en', 'es'] }, rucNumber: { type: 'string', maxLength: 15 }, }, }, }, }, }, responses: { 200: { description: 'Profile updated' }, 401: { description: 'Unauthorized' }, }, }, }, '/api/dashboard/tickets': { get: { tags: ['User Dashboard'], summary: 'Get user tickets', description: 'Get all tickets for the authenticated user with event and payment details.', security: [{ bearerAuth: [] }], responses: { 200: { description: 'List of user tickets' }, 401: { description: 'Unauthorized' }, }, }, }, '/api/dashboard/tickets/{id}': { get: { tags: ['User Dashboard'], summary: 'Get ticket detail', description: 'Get detailed information about a specific ticket.', security: [{ bearerAuth: [] }], parameters: [ { name: 'id', in: 'path', required: true, schema: { type: 'string' } }, ], responses: { 200: { description: 'Ticket details' }, 404: { description: 'Ticket not found' }, 401: { description: 'Unauthorized' }, }, }, }, '/api/dashboard/next-event': { get: { tags: ['User Dashboard'], summary: 'Get next upcoming event', description: 'Get the next upcoming event the user has a ticket for.', security: [{ bearerAuth: [] }], responses: { 200: { description: 'Next event info or null' }, 401: { description: 'Unauthorized' }, }, }, }, '/api/dashboard/payments': { get: { tags: ['User Dashboard'], summary: 'Get payment history', description: 'Get all payments made by the user.', security: [{ bearerAuth: [] }], responses: { 200: { description: 'List of payments' }, 401: { description: 'Unauthorized' }, }, }, }, '/api/dashboard/invoices': { get: { tags: ['User Dashboard'], summary: 'Get invoices', description: 'Get all invoices for the user.', security: [{ bearerAuth: [] }], responses: { 200: { description: 'List of invoices' }, 401: { description: 'Unauthorized' }, }, }, }, '/api/dashboard/sessions': { get: { tags: ['User Dashboard'], summary: 'Get active sessions', description: 'Get all active login sessions for the user.', security: [{ bearerAuth: [] }], responses: { 200: { description: 'List of sessions' }, 401: { description: 'Unauthorized' }, }, }, }, '/api/dashboard/sessions/{id}': { delete: { tags: ['User Dashboard'], summary: 'Revoke session', description: 'Revoke a specific session.', security: [{ bearerAuth: [] }], parameters: [ { name: 'id', in: 'path', required: true, schema: { type: 'string' } }, ], responses: { 200: { description: 'Session revoked' }, 401: { description: 'Unauthorized' }, }, }, }, '/api/dashboard/sessions/revoke-all': { post: { tags: ['User Dashboard'], summary: 'Revoke all sessions', description: 'Logout from all devices.', security: [{ bearerAuth: [] }], responses: { 200: { description: 'All sessions revoked' }, 401: { description: 'Unauthorized' }, }, }, }, '/api/dashboard/set-password': { post: { tags: ['User Dashboard'], summary: 'Set password', description: 'Set a password for users who signed up via Google only.', security: [{ bearerAuth: [] }], requestBody: { required: true, content: { 'application/json': { schema: { type: 'object', required: ['password'], properties: { password: { type: 'string', minLength: 10 }, }, }, }, }, }, responses: { 200: { description: 'Password set' }, 400: { description: 'Password already set' }, 401: { description: 'Unauthorized' }, }, }, }, '/api/dashboard/unlink-google': { post: { tags: ['User Dashboard'], summary: 'Unlink Google account', description: 'Unlink Google account. Requires password to be set first.', security: [{ bearerAuth: [] }], responses: { 200: { description: 'Google unlinked' }, 400: { description: 'Cannot unlink without password' }, 401: { description: 'Unauthorized' }, }, }, }, // ==================== Events Endpoints ==================== '/api/events': { get: { tags: ['Events'], summary: 'Get all events', description: 'Get list of events with optional filters.', parameters: [ { name: 'status', in: 'query', schema: { type: 'string', enum: ['draft', 'published', 'cancelled', 'completed', 'archived'] } }, { name: 'upcoming', in: 'query', schema: { type: 'boolean' }, description: 'Filter to only future events' }, ], responses: { 200: { description: 'List of events' }, }, }, post: { tags: ['Events'], summary: 'Create event', description: 'Create a new event (admin/organizer only).', security: [{ bearerAuth: [] }], requestBody: { required: true, content: { 'application/json': { schema: { type: 'object', required: ['title', 'description', 'startDatetime', 'location'], properties: { title: { type: 'string' }, titleEs: { type: 'string' }, description: { type: 'string' }, descriptionEs: { type: 'string' }, startDatetime: { type: 'string', format: 'date-time' }, endDatetime: { type: 'string', format: 'date-time' }, location: { type: 'string' }, locationUrl: { type: 'string', format: 'uri' }, price: { type: 'number' }, currency: { type: 'string', default: 'PYG' }, capacity: { type: 'integer', default: 50 }, status: { type: 'string', enum: ['draft', 'published', 'cancelled', 'completed', 'archived'] }, bannerUrl: { type: 'string', format: 'uri' }, }, }, }, }, }, responses: { 201: { description: 'Event created' }, 401: { description: 'Unauthorized' }, 403: { description: 'Forbidden' }, }, }, }, '/api/events/{id}': { get: { tags: ['Events'], summary: 'Get event by ID', parameters: [ { name: 'id', in: 'path', required: true, schema: { type: 'string' } }, ], responses: { 200: { description: 'Event details' }, 404: { description: 'Event not found' }, }, }, put: { tags: ['Events'], summary: 'Update event', security: [{ bearerAuth: [] }], parameters: [ { name: 'id', in: 'path', required: true, schema: { type: 'string' } }, ], responses: { 200: { description: 'Event updated' }, 404: { description: 'Event not found' }, }, }, delete: { tags: ['Events'], summary: 'Delete event', security: [{ bearerAuth: [] }], parameters: [ { name: 'id', in: 'path', required: true, schema: { type: 'string' } }, ], responses: { 200: { description: 'Event deleted' }, 404: { description: 'Event not found' }, }, }, }, '/api/events/next/upcoming': { get: { tags: ['Events'], summary: 'Get next upcoming event', description: 'Get the single next upcoming published event.', responses: { 200: { description: 'Next event or null' }, }, }, }, '/api/events/{id}/duplicate': { post: { tags: ['Events'], summary: 'Duplicate event', description: 'Create a copy of an existing event.', security: [{ bearerAuth: [] }], parameters: [ { name: 'id', in: 'path', required: true, schema: { type: 'string' } }, ], responses: { 201: { description: 'Event duplicated' }, 404: { description: 'Event not found' }, }, }, }, // ==================== Tickets Endpoints ==================== '/api/tickets': { get: { tags: ['Tickets'], summary: 'Get all tickets (admin)', security: [{ bearerAuth: [] }], parameters: [ { name: 'eventId', in: 'query', schema: { type: 'string' } }, { name: 'status', in: 'query', schema: { type: 'string', enum: ['pending', 'confirmed', 'cancelled', 'checked_in'] } }, ], responses: { 200: { description: 'List of tickets' }, }, }, post: { tags: ['Tickets'], summary: 'Book a ticket', description: 'Create a booking for an event. Creates user account if needed.', requestBody: { required: true, content: { 'application/json': { schema: { type: 'object', required: ['eventId', 'firstName', 'email', 'paymentMethod'], properties: { eventId: { type: 'string' }, firstName: { type: 'string' }, lastName: { type: 'string' }, email: { type: 'string', format: 'email' }, phone: { type: 'string' }, preferredLanguage: { type: 'string', enum: ['en', 'es'] }, paymentMethod: { type: 'string', enum: ['lightning', 'cash', 'bank_transfer', 'tpago'] }, ruc: { type: 'string', description: 'Paraguayan RUC for invoice' }, }, }, }, }, }, responses: { 201: { description: 'Ticket booked' }, 400: { description: 'Booking error' }, }, }, }, '/api/tickets/{id}': { get: { tags: ['Tickets'], summary: 'Get ticket by ID', parameters: [ { name: 'id', in: 'path', required: true, schema: { type: 'string' } }, ], responses: { 200: { description: 'Ticket details' }, 404: { description: 'Ticket not found' }, }, }, put: { tags: ['Tickets'], summary: 'Update ticket', security: [{ bearerAuth: [] }], parameters: [ { name: 'id', in: 'path', required: true, schema: { type: 'string' } }, ], responses: { 200: { description: 'Ticket updated' }, }, }, }, '/api/tickets/{id}/checkin': { post: { tags: ['Tickets'], summary: 'Check in ticket', security: [{ bearerAuth: [] }], parameters: [ { name: 'id', in: 'path', required: true, schema: { type: 'string' } }, ], responses: { 200: { description: 'Check-in successful' }, 400: { description: 'Check-in error' }, }, }, }, '/api/tickets/{id}/remove-checkin': { post: { tags: ['Tickets'], summary: 'Remove check-in', security: [{ bearerAuth: [] }], parameters: [ { name: 'id', in: 'path', required: true, schema: { type: 'string' } }, ], responses: { 200: { description: 'Check-in removed' }, }, }, }, '/api/tickets/{id}/cancel': { post: { tags: ['Tickets'], summary: 'Cancel ticket', security: [{ bearerAuth: [] }], parameters: [ { name: 'id', in: 'path', required: true, schema: { type: 'string' } }, ], responses: { 200: { description: 'Ticket cancelled' }, }, }, }, '/api/tickets/{id}/mark-paid': { post: { tags: ['Tickets'], summary: 'Mark ticket as paid (admin)', security: [{ bearerAuth: [] }], parameters: [ { name: 'id', in: 'path', required: true, schema: { type: 'string' } }, ], responses: { 200: { description: 'Marked as paid' }, }, }, }, '/api/tickets/{id}/mark-payment-sent': { post: { tags: ['Tickets'], summary: 'Mark payment sent', description: 'User marks their bank transfer or TPago payment as sent.', parameters: [ { name: 'id', in: 'path', required: true, schema: { type: 'string' } }, ], responses: { 200: { description: 'Payment marked as pending approval' }, }, }, }, '/api/tickets/{id}/note': { post: { tags: ['Tickets'], summary: 'Update ticket note', security: [{ bearerAuth: [] }], parameters: [ { name: 'id', in: 'path', required: true, schema: { type: 'string' } }, ], requestBody: { required: true, content: { 'application/json': { schema: { type: 'object', properties: { note: { type: 'string' }, }, }, }, }, }, responses: { 200: { description: 'Note updated' }, }, }, }, '/api/tickets/admin/create': { post: { tags: ['Tickets'], summary: 'Admin create ticket', description: 'Create ticket directly without payment (admin only).', security: [{ bearerAuth: [] }], requestBody: { required: true, content: { 'application/json': { schema: { type: 'object', required: ['eventId', 'firstName'], properties: { eventId: { type: 'string' }, firstName: { type: 'string' }, lastName: { type: 'string' }, email: { type: 'string', format: 'email' }, phone: { type: 'string' }, preferredLanguage: { type: 'string', enum: ['en', 'es'] }, autoCheckin: { type: 'boolean' }, adminNote: { type: 'string' }, }, }, }, }, }, responses: { 201: { description: 'Ticket created' }, }, }, }, // ==================== Payments Endpoints ==================== '/api/payments': { get: { tags: ['Payments'], summary: 'Get all payments', security: [{ bearerAuth: [] }], parameters: [ { name: 'status', in: 'query', schema: { type: 'string' } }, { name: 'provider', in: 'query', schema: { type: 'string' } }, { name: 'pendingApproval', in: 'query', schema: { type: 'boolean' } }, ], responses: { 200: { description: 'List of payments' }, }, }, }, '/api/payments/pending-approval': { get: { tags: ['Payments'], summary: 'Get pending approval payments', security: [{ bearerAuth: [] }], responses: { 200: { description: 'Payments awaiting approval' }, }, }, }, '/api/payments/{id}': { get: { tags: ['Payments'], summary: 'Get payment by ID', security: [{ bearerAuth: [] }], parameters: [ { name: 'id', in: 'path', required: true, schema: { type: 'string' } }, ], responses: { 200: { description: 'Payment details' }, }, }, put: { tags: ['Payments'], summary: 'Update payment', security: [{ bearerAuth: [] }], parameters: [ { name: 'id', in: 'path', required: true, schema: { type: 'string' } }, ], responses: { 200: { description: 'Payment updated' }, }, }, }, '/api/payments/{id}/approve': { post: { tags: ['Payments'], summary: 'Approve payment', security: [{ bearerAuth: [] }], parameters: [ { name: 'id', in: 'path', required: true, schema: { type: 'string' } }, ], requestBody: { content: { 'application/json': { schema: { type: 'object', properties: { adminNote: { type: 'string' }, }, }, }, }, }, responses: { 200: { description: 'Payment approved' }, }, }, }, '/api/payments/{id}/reject': { post: { tags: ['Payments'], summary: 'Reject payment', security: [{ bearerAuth: [] }], parameters: [ { name: 'id', in: 'path', required: true, schema: { type: 'string' } }, ], requestBody: { content: { 'application/json': { schema: { type: 'object', properties: { adminNote: { type: 'string' }, }, }, }, }, }, responses: { 200: { description: 'Payment rejected' }, }, }, }, '/api/payments/{id}/refund': { post: { tags: ['Payments'], summary: 'Refund payment', security: [{ bearerAuth: [] }], parameters: [ { name: 'id', in: 'path', required: true, schema: { type: 'string' } }, ], responses: { 200: { description: 'Refund processed' }, }, }, }, '/api/payments/{id}/note': { post: { tags: ['Payments'], summary: 'Update payment note', security: [{ bearerAuth: [] }], parameters: [ { name: 'id', in: 'path', required: true, schema: { type: 'string' } }, ], requestBody: { required: true, content: { 'application/json': { schema: { type: 'object', properties: { adminNote: { type: 'string' }, }, }, }, }, }, responses: { 200: { description: 'Note updated' }, }, }, }, // ==================== Payment Options Endpoints ==================== '/api/payment-options': { get: { tags: ['Payment Options'], summary: 'Get global payment options', responses: { 200: { description: 'Payment options configuration' }, }, }, put: { tags: ['Payment Options'], summary: 'Update global payment options', security: [{ bearerAuth: [] }], requestBody: { required: true, content: { 'application/json': { schema: { type: 'object', properties: { tpagoEnabled: { type: 'boolean' }, tpagoLink: { type: 'string' }, tpagoInstructions: { type: 'string' }, tpagoInstructionsEs: { type: 'string' }, bankTransferEnabled: { type: 'boolean' }, bankName: { type: 'string' }, bankAccountHolder: { type: 'string' }, bankAccountNumber: { type: 'string' }, bankAlias: { type: 'string' }, bankPhone: { type: 'string' }, bankNotes: { type: 'string' }, bankNotesEs: { type: 'string' }, lightningEnabled: { type: 'boolean' }, cashEnabled: { type: 'boolean' }, cashInstructions: { type: 'string' }, cashInstructionsEs: { type: 'string' }, }, }, }, }, }, responses: { 200: { description: 'Options updated' }, }, }, }, '/api/payment-options/event/{eventId}': { get: { tags: ['Payment Options'], summary: 'Get payment options for event', description: 'Get merged payment options (global + event overrides).', parameters: [ { name: 'eventId', in: 'path', required: true, schema: { type: 'string' } }, ], responses: { 200: { description: 'Payment options for event' }, }, }, }, '/api/payment-options/event/{eventId}/overrides': { get: { tags: ['Payment Options'], summary: 'Get event payment overrides', security: [{ bearerAuth: [] }], parameters: [ { name: 'eventId', in: 'path', required: true, schema: { type: 'string' } }, ], responses: { 200: { description: 'Event-specific overrides' }, }, }, put: { tags: ['Payment Options'], summary: 'Update event payment overrides', security: [{ bearerAuth: [] }], parameters: [ { name: 'eventId', in: 'path', required: true, schema: { type: 'string' } }, ], responses: { 200: { description: 'Overrides updated' }, }, }, delete: { tags: ['Payment Options'], summary: 'Delete event payment overrides', security: [{ bearerAuth: [] }], parameters: [ { name: 'eventId', in: 'path', required: true, schema: { type: 'string' } }, ], responses: { 200: { description: 'Overrides deleted' }, }, }, }, // ==================== Users Endpoints (Admin) ==================== '/api/users': { get: { tags: ['Users'], summary: 'Get all users (admin)', security: [{ bearerAuth: [] }], parameters: [ { name: 'role', in: 'query', schema: { type: 'string' } }, ], responses: { 200: { description: 'List of users' }, }, }, }, '/api/users/{id}': { get: { tags: ['Users'], summary: 'Get user by ID', security: [{ bearerAuth: [] }], parameters: [ { name: 'id', in: 'path', required: true, schema: { type: 'string' } }, ], responses: { 200: { description: 'User details' }, }, }, put: { tags: ['Users'], summary: 'Update user', security: [{ bearerAuth: [] }], parameters: [ { name: 'id', in: 'path', required: true, schema: { type: 'string' } }, ], responses: { 200: { description: 'User updated' }, }, }, delete: { tags: ['Users'], summary: 'Delete user', security: [{ bearerAuth: [] }], parameters: [ { name: 'id', in: 'path', required: true, schema: { type: 'string' } }, ], responses: { 200: { description: 'User deleted' }, }, }, }, '/api/users/{id}/history': { get: { tags: ['Users'], summary: 'Get user ticket history', security: [{ bearerAuth: [] }], parameters: [ { name: 'id', in: 'path', required: true, schema: { type: 'string' } }, ], responses: { 200: { description: 'User ticket history' }, }, }, }, '/api/users/stats/overview': { get: { tags: ['Users'], summary: 'Get user statistics', security: [{ bearerAuth: [] }], responses: { 200: { description: 'User statistics' }, }, }, }, // ==================== Contacts Endpoints ==================== '/api/contacts': { get: { tags: ['Contacts'], summary: 'Get all contacts (admin)', security: [{ bearerAuth: [] }], parameters: [ { name: 'status', in: 'query', schema: { type: 'string', enum: ['new', 'read', 'replied'] } }, ], responses: { 200: { description: 'List of contacts' }, }, }, post: { tags: ['Contacts'], summary: 'Submit contact form', requestBody: { required: true, content: { 'application/json': { schema: { type: 'object', required: ['name', 'email', 'message'], properties: { name: { type: 'string' }, email: { type: 'string', format: 'email' }, message: { type: 'string', minLength: 10 }, }, }, }, }, }, responses: { 201: { description: 'Message sent' }, }, }, }, '/api/contacts/{id}': { put: { tags: ['Contacts'], summary: 'Update contact status', security: [{ bearerAuth: [] }], parameters: [ { name: 'id', in: 'path', required: true, schema: { type: 'string' } }, ], requestBody: { required: true, content: { 'application/json': { schema: { type: 'object', properties: { status: { type: 'string', enum: ['new', 'read', 'replied'] }, }, }, }, }, }, responses: { 200: { description: 'Contact updated' }, }, }, }, '/api/contacts/subscribe': { post: { tags: ['Contacts'], summary: 'Subscribe to newsletter', requestBody: { required: true, content: { 'application/json': { schema: { type: 'object', required: ['email'], properties: { email: { type: 'string', format: 'email' }, name: { type: 'string' }, }, }, }, }, }, responses: { 201: { description: 'Subscribed successfully' }, }, }, }, // ==================== Email Endpoints ==================== '/api/emails/templates': { get: { tags: ['Emails'], summary: 'Get all email templates', security: [{ bearerAuth: [] }], responses: { 200: { description: 'List of templates' }, }, }, post: { tags: ['Emails'], summary: 'Create email template', security: [{ bearerAuth: [] }], responses: { 201: { description: 'Template created' }, }, }, }, '/api/emails/templates/{id}': { get: { tags: ['Emails'], summary: 'Get template by ID', security: [{ bearerAuth: [] }], parameters: [ { name: 'id', in: 'path', required: true, schema: { type: 'string' } }, ], responses: { 200: { description: 'Template details' }, }, }, put: { tags: ['Emails'], summary: 'Update template', security: [{ bearerAuth: [] }], parameters: [ { name: 'id', in: 'path', required: true, schema: { type: 'string' } }, ], responses: { 200: { description: 'Template updated' }, }, }, delete: { tags: ['Emails'], summary: 'Delete template', security: [{ bearerAuth: [] }], parameters: [ { name: 'id', in: 'path', required: true, schema: { type: 'string' } }, ], responses: { 200: { description: 'Template deleted' }, }, }, }, '/api/emails/send/event/{eventId}': { post: { tags: ['Emails'], summary: 'Send email to event attendees', security: [{ bearerAuth: [] }], parameters: [ { name: 'eventId', in: 'path', required: true, schema: { type: 'string' } }, ], requestBody: { required: true, content: { 'application/json': { schema: { type: 'object', required: ['templateSlug'], properties: { templateSlug: { type: 'string' }, customVariables: { type: 'object' }, recipientFilter: { type: 'string', enum: ['all', 'confirmed', 'pending', 'checked_in'] }, }, }, }, }, }, responses: { 200: { description: 'Emails sent' }, }, }, }, '/api/emails/send/custom': { post: { tags: ['Emails'], summary: 'Send custom email', security: [{ bearerAuth: [] }], requestBody: { required: true, content: { 'application/json': { schema: { type: 'object', required: ['to', 'subject', 'bodyHtml'], properties: { to: { type: 'string', format: 'email' }, toName: { type: 'string' }, subject: { type: 'string' }, bodyHtml: { type: 'string' }, bodyText: { type: 'string' }, eventId: { type: 'string' }, }, }, }, }, }, responses: { 200: { description: 'Email sent' }, }, }, }, '/api/emails/preview': { post: { tags: ['Emails'], summary: 'Preview email template', security: [{ bearerAuth: [] }], requestBody: { required: true, content: { 'application/json': { schema: { type: 'object', required: ['templateSlug'], properties: { templateSlug: { type: 'string' }, variables: { type: 'object' }, locale: { type: 'string', enum: ['en', 'es'] }, }, }, }, }, }, responses: { 200: { description: 'Preview HTML' }, }, }, }, '/api/emails/logs': { get: { tags: ['Emails'], summary: 'Get email logs', security: [{ bearerAuth: [] }], parameters: [ { name: 'eventId', in: 'query', schema: { type: 'string' } }, { name: 'status', in: 'query', schema: { type: 'string' } }, { name: 'limit', in: 'query', schema: { type: 'integer' } }, { name: 'offset', in: 'query', schema: { type: 'integer' } }, ], responses: { 200: { description: 'Email logs' }, }, }, }, '/api/emails/stats': { get: { tags: ['Emails'], summary: 'Get email stats', security: [{ bearerAuth: [] }], parameters: [ { name: 'eventId', in: 'query', schema: { type: 'string' } }, ], responses: { 200: { description: 'Email statistics' }, }, }, }, '/api/emails/seed-templates': { post: { tags: ['Emails'], summary: 'Seed default templates', security: [{ bearerAuth: [] }], responses: { 200: { description: 'Templates seeded' }, }, }, }, // ==================== Media Endpoints ==================== '/api/media/upload': { post: { tags: ['Media'], summary: 'Upload file', security: [{ bearerAuth: [] }], requestBody: { required: true, content: { 'multipart/form-data': { schema: { type: 'object', properties: { file: { type: 'string', format: 'binary' }, relatedId: { type: 'string' }, relatedType: { type: 'string' }, }, }, }, }, }, responses: { 201: { description: 'File uploaded' }, }, }, }, '/api/media/{id}': { delete: { tags: ['Media'], summary: 'Delete media', security: [{ bearerAuth: [] }], parameters: [ { name: 'id', in: 'path', required: true, schema: { type: 'string' } }, ], responses: { 200: { description: 'Media deleted' }, }, }, }, // ==================== Lightning (LNBits) Endpoints ==================== '/api/lnbits/invoice': { post: { tags: ['Lightning'], summary: 'Create Lightning invoice', description: 'Create a Lightning Network invoice for payment.', requestBody: { required: true, content: { 'application/json': { schema: { type: 'object', required: ['ticketId'], properties: { ticketId: { type: 'string' }, }, }, }, }, }, responses: { 200: { description: 'Invoice created' }, }, }, }, '/api/lnbits/status/{ticketId}': { get: { tags: ['Lightning'], summary: 'Check payment status', description: 'Check the payment status for a ticket.', parameters: [ { name: 'ticketId', in: 'path', required: true, schema: { type: 'string' } }, ], responses: { 200: { description: 'Payment status' }, }, }, }, '/api/lnbits/webhook': { post: { tags: ['Lightning'], summary: 'LNBits webhook', description: 'Webhook endpoint for LNBits payment notifications.', responses: { 200: { description: 'Webhook processed' }, }, }, }, // ==================== Admin Endpoints ==================== '/api/admin/dashboard': { get: { tags: ['Admin'], summary: 'Get admin dashboard', description: 'Get statistics, recent activity, and overview data.', security: [{ bearerAuth: [] }], responses: { 200: { description: 'Dashboard data' }, }, }, }, '/api/admin/analytics': { get: { tags: ['Admin'], summary: 'Get analytics', description: 'Get detailed analytics data.', security: [{ bearerAuth: [] }], responses: { 200: { description: 'Analytics data' }, }, }, }, '/api/admin/export/tickets': { get: { tags: ['Admin'], summary: 'Export tickets', security: [{ bearerAuth: [] }], parameters: [ { name: 'eventId', in: 'query', schema: { type: 'string' } }, ], responses: { 200: { description: 'Exported ticket data' }, }, }, }, '/api/admin/export/financial': { get: { tags: ['Admin'], summary: 'Export financial data', security: [{ bearerAuth: [] }], parameters: [ { name: 'startDate', in: 'query', schema: { type: 'string', format: 'date' } }, { name: 'endDate', in: 'query', schema: { type: 'string', format: 'date' } }, { name: 'eventId', in: 'query', schema: { type: 'string' } }, ], responses: { 200: { description: 'Exported financial data' }, }, }, }, }, components: { securitySchemes: { bearerAuth: { type: 'http', scheme: 'bearer', bearerFormat: 'JWT', description: 'JWT token obtained from login endpoint', }, }, schemas: { User: { type: 'object', properties: { id: { type: 'string' }, email: { type: 'string' }, name: { type: 'string' }, phone: { type: 'string' }, role: { type: 'string', enum: ['admin', 'organizer', 'staff', 'marketing', 'user'] }, languagePreference: { type: 'string' }, isClaimed: { type: 'boolean' }, rucNumber: { type: 'string' }, accountStatus: { type: 'string', enum: ['active', 'unclaimed', 'suspended'] }, createdAt: { type: 'string', format: 'date-time' }, }, }, Event: { type: 'object', properties: { id: { type: 'string' }, title: { type: 'string' }, titleEs: { type: 'string' }, description: { type: 'string' }, descriptionEs: { type: 'string' }, startDatetime: { type: 'string', format: 'date-time' }, endDatetime: { type: 'string', format: 'date-time' }, location: { type: 'string' }, locationUrl: { type: 'string' }, price: { type: 'number' }, currency: { type: 'string' }, capacity: { type: 'integer' }, status: { type: 'string', enum: ['draft', 'published', 'cancelled', 'completed', 'archived'] }, bannerUrl: { type: 'string' }, createdAt: { type: 'string', format: 'date-time' }, }, }, Ticket: { type: 'object', properties: { id: { type: 'string' }, userId: { type: 'string' }, eventId: { type: 'string' }, attendeeFirstName: { type: 'string' }, attendeeLastName: { type: 'string' }, attendeeEmail: { type: 'string' }, attendeePhone: { type: 'string' }, attendeeRuc: { type: 'string' }, preferredLanguage: { type: 'string' }, status: { type: 'string', enum: ['pending', 'confirmed', 'cancelled', 'checked_in'] }, qrCode: { type: 'string' }, checkinAt: { type: 'string', format: 'date-time' }, createdAt: { type: 'string', format: 'date-time' }, }, }, Payment: { type: 'object', properties: { id: { type: 'string' }, ticketId: { type: 'string' }, provider: { type: 'string', enum: ['lightning', 'cash', 'bank_transfer', 'tpago'] }, amount: { type: 'number' }, currency: { type: 'string' }, status: { type: 'string', enum: ['pending', 'pending_approval', 'paid', 'refunded', 'failed', 'cancelled'] }, reference: { type: 'string' }, paidAt: { type: 'string', format: 'date-time' }, createdAt: { type: 'string', format: 'date-time' }, }, }, Invoice: { type: 'object', properties: { id: { type: 'string' }, paymentId: { type: 'string' }, userId: { type: 'string' }, invoiceNumber: { type: 'string' }, rucNumber: { type: 'string' }, legalName: { type: 'string' }, amount: { type: 'number' }, currency: { type: 'string' }, pdfUrl: { type: 'string' }, status: { type: 'string', enum: ['generated', 'voided'] }, createdAt: { type: 'string', format: 'date-time' }, }, }, }, }, }; // OpenAPI JSON endpoint app.get('/openapi.json', (c) => { return c.json(openApiSpec); }); // Swagger UI app.get('/api-docs', swaggerUI({ url: '/openapi.json' })); // Static file serving for uploads app.use('/uploads/*', serveStatic({ root: './' })); // Health check app.get('/health', (c) => { return c.json({ status: 'ok', timestamp: new Date().toISOString() }); }); // API Routes app.route('/api/auth', authRoutes); app.route('/api/events', eventsRoutes); app.route('/api/tickets', ticketsRoutes); app.route('/api/users', usersRoutes); app.route('/api/contacts', contactsRoutes); app.route('/api/payments', paymentsRoutes); app.route('/api/admin', adminRoutes); app.route('/api/media', mediaRoutes); app.route('/api/lnbits', lnbitsRoutes); app.route('/api/emails', emailsRoutes); app.route('/api/payment-options', paymentOptionsRoutes); app.route('/api/dashboard', dashboardRoutes); // 404 handler app.notFound((c) => { return c.json({ error: 'Not Found' }, 404); }); // Error handler app.onError((err, c) => { console.error('Error:', err); return c.json({ error: 'Internal Server Error' }, 500); }); const port = parseInt(process.env.PORT || '3001'); // Initialize email templates on startup emailService.seedDefaultTemplates().catch(err => { console.error('[Email] Failed to seed templates:', err); }); console.log(`🚀 Spanglish API server starting on port ${port}`); console.log(`📚 API docs available at http://localhost:${port}/api-docs`); console.log(`📋 OpenAPI spec at http://localhost:${port}/openapi.json`); serve({ fetch: app.fetch, port, });