← back to Jill Website

src/routes/inquiries.ts

156 lines

import { Router, Request, Response } from 'express';
import { InquiryModel, Inquiry, InquiryValidationError } from '../models/Inquiry';
import { emailService, EmailService } from '../services/emailService';
import { requireAuth } from '../middleware/auth';

// Factory function for testing
export function createInquiriesRouter(inquiryModel?: InquiryModel, emailSvc?: EmailService): Router {
  const router = Router();
  const model = inquiryModel || new InquiryModel();
  const emailServiceInstance = emailSvc || emailService;

  /**
   * POST /api/inquiries
   * Create a new guest inquiry
   */
  router.post('/inquiries', async (req: Request, res: Response): Promise<void> => {
    try {
      if (!req.body) {
        res.status(400).json({
          success: false,
          error: 'Validation error',
          message: 'Request body is required'
        });
        return;
      }

      const inquiryData: Omit<Inquiry, 'id' | 'created_at' | 'updated_at'> = {
        name: req.body.name,
        email: req.body.email,
        phone: req.body.phone,
        check_in_date: req.body.check_in_date ? new Date(req.body.check_in_date) : undefined as any,
        check_out_date: req.body.check_out_date ? new Date(req.body.check_out_date) : undefined as any,
        guest_count: req.body.guest_count ? parseInt(req.body.guest_count, 10) : undefined as any,
        message: req.body.message
      };

      // Create inquiry (validation happens in model)
      const newInquiry = await model.create(inquiryData);

      // Send confirmation email (non-blocking)
      emailServiceInstance.sendConfirmationEmail(newInquiry)
        .then(result => {
          if (!result.success) {
            console.error('Failed to send confirmation email:', result.error);
          } else {
            console.log('Confirmation email sent successfully:', result.messageId);
          }
        })
        .catch(error => {
          console.error('Error sending confirmation email:', error);
        });

      res.status(201).json({
        success: true,
        data: {
          id: newInquiry.id,
          inquiry: newInquiry
        },
        message: 'Inquiry created successfully'
      });
    } catch (error) {
      // Handle validation errors
      if (error instanceof Error && error.message.startsWith('Validation failed:')) {
        res.status(400).json({
          success: false,
          error: 'Validation error',
          message: error.message
        });
        return;
      }

      // Handle other errors
      console.error('Error creating inquiry:', error);
      res.status(500).json({
        success: false,
        error: 'Internal server error',
        message: 'Failed to create inquiry'
      });
    }
  });

  /**
   * GET /api/inquiries — admin-only PII data dump (name, email, phone, message
   * of every guest inquiry). P0 fix 2026-05-04: was unauth (only the site-wide
   * basic-auth gate stood between it and any visitor with the site password).
   */
  router.get('/inquiries', requireAuth, async (req: Request, res: Response): Promise<void> => {
    try {
      const limit = req.query.limit ? parseInt(req.query.limit as string, 10) : 100;
      const offset = req.query.offset ? parseInt(req.query.offset as string, 10) : 0;

      const inquiries = await model.findAll(limit, offset);

      res.status(200).json({
        success: true,
        data: inquiries,
        count: inquiries.length
      });
    } catch (error) {
      console.error('Error fetching inquiries:', error);
      res.status(500).json({
        success: false,
        error: 'Internal server error',
        message: 'Failed to fetch inquiries'
      });
    }
  });

  /**
   * GET /api/inquiries/:id — admin-only (P0 fix 2026-05-04, same as the list route).
   */
  router.get('/inquiries/:id', requireAuth, async (req: Request, res: Response): Promise<void> => {
    try {
      const idParam = Array.isArray(req.params.id) ? req.params.id[0] : req.params.id;
      const id = parseInt(idParam, 10);

      // Check if the ID is a valid integer (not NaN, not a decimal)
      if (isNaN(id) || idParam !== id.toString()) {
        res.status(400).json({
          success: false,
          error: 'Invalid ID',
          message: 'Inquiry ID must be a number'
        });
        return;
      }

      const inquiry = await model.findById(id);

      if (!inquiry) {
        res.status(404).json({
          success: false,
          error: 'Not found',
          message: 'Inquiry not found'
        });
        return;
      }

      res.status(200).json({
        success: true,
        data: inquiry
      });
    } catch (error) {
      console.error('Error fetching inquiry:', error);
      res.status(500).json({
        success: false,
        error: 'Internal server error',
        message: 'Failed to fetch inquiry'
      });
    }
  });

  return router;
}

export default createInquiriesRouter();