← back to Jill Website

src/routes/linkValidation.ts

275 lines

import { Router, Request, Response } from 'express';
import { LinkCheckModel } from '../models/LinkCheck';
import { PropertyModel } from '../models/Property';
import { ActivityModel } from '../models/Activity';
import { linkValidationService } from '../services/linkValidationService';
import { requireAuth } from '../middleware/auth';

export function createLinkValidationRouter(
  linkCheckModel?: LinkCheckModel,
  propertyModel?: PropertyModel,
  activityModel?: ActivityModel
): Router {
  const router = Router();
  // P0 fix 2026-05-04: gate the entire link-validation router with admin auth.
  // /check-url was an unauth SSRF (POST any URL → server fetched it). Even with
  // the new IP block in linkValidationService.checkLink, this route should be
  // admin-only — link validation is an internal operations tool.
  router.use(requireAuth);
  const linkChecks = linkCheckModel || new LinkCheckModel();
  const properties = propertyModel || new PropertyModel();
  const activities = activityModel || new ActivityModel();

  /**
   * GET /api/link-validation/stats
   * Get link validation statistics
   */
  router.get('/stats', async (req: Request, res: Response) => {
    try {
      const stats = await linkChecks.getStats();
      res.json({
        success: true,
        data: stats,
      });
    } catch (error) {
      console.error('Error getting link validation stats:', error);
      res.status(500).json({
        success: false,
        error: 'Failed to get stats',
        message: error instanceof Error ? error.message : 'Unknown error',
      });
    }
  });

  /**
   * GET /api/link-validation/broken
   * Get all broken links
   */
  router.get('/broken', async (req: Request, res: Response) => {
    try {
      const limit = parseInt(req.query.limit as string) || 100;
      const offset = parseInt(req.query.offset as string) || 0;

      const brokenLinks = await linkChecks.findBrokenLinks(limit, offset);

      res.json({
        success: true,
        data: brokenLinks,
        count: brokenLinks.length,
      });
    } catch (error) {
      console.error('Error getting broken links:', error);
      res.status(500).json({
        success: false,
        error: 'Failed to get broken links',
        message: error instanceof Error ? error.message : 'Unknown error',
      });
    }
  });

  /**
   * GET /api/link-validation/all
   * Get all link check records
   */
  router.get('/all', async (req: Request, res: Response) => {
    try {
      const limit = parseInt(req.query.limit as string) || 100;
      const offset = parseInt(req.query.offset as string) || 0;

      const allLinks = await linkChecks.findAll(limit, offset);

      res.json({
        success: true,
        data: allLinks,
        count: allLinks.length,
      });
    } catch (error) {
      console.error('Error getting all link checks:', error);
      res.status(500).json({
        success: false,
        error: 'Failed to get link checks',
        message: error instanceof Error ? error.message : 'Unknown error',
      });
    }
  });

  /**
   * POST /api/link-validation/check-all
   * Manually trigger a full link validation check
   */
  router.post('/check-all', async (req: Request, res: Response) => {
    try {
      const results: any[] = [];

      // Check all property links
      const allProperties = await properties.findAll(1000, 0);
      for (const property of allProperties) {
        // Check Airbnb URL
        if (property.airbnb_url) {
          const checkResult = await linkValidationService.checkLink(property.airbnb_url);
          await linkChecks.upsert({
            url: property.airbnb_url,
            source_type: 'property',
            source_id: property.id!,
            http_status: checkResult.httpStatus,
            is_valid: checkResult.isValid,
            error_message: checkResult.errorMessage,
            response_time_ms: checkResult.responseTimeMs,
          });
          results.push({
            source: 'property',
            source_id: property.id,
            ...checkResult,
          });
        }

        // Check image URLs
        if (property.images && property.images.length > 0) {
          for (const imageUrl of property.images) {
            const checkResult = await linkValidationService.checkLink(imageUrl);
            await linkChecks.upsert({
              url: imageUrl,
              source_type: 'property_image',
              source_id: property.id!,
              http_status: checkResult.httpStatus,
              is_valid: checkResult.isValid,
              error_message: checkResult.errorMessage,
              response_time_ms: checkResult.responseTimeMs,
            });
            results.push({
              source: 'property_image',
              source_id: property.id,
              ...checkResult,
            });
          }
        }
      }

      // Check all activity links
      const allActivities = await activities.findAll(1000, 0);
      for (const activity of allActivities) {
        // Check website URL
        if (activity.website) {
          const checkResult = await linkValidationService.checkLink(activity.website);
          await linkChecks.upsert({
            url: activity.website,
            source_type: 'activity_website',
            source_id: activity.id!,
            http_status: checkResult.httpStatus,
            is_valid: checkResult.isValid,
            error_message: checkResult.errorMessage,
            response_time_ms: checkResult.responseTimeMs,
          });
          results.push({
            source: 'activity_website',
            source_id: activity.id,
            ...checkResult,
          });
        }

        // Check maps link
        if (activity.mapsLink) {
          const checkResult = await linkValidationService.checkLink(activity.mapsLink);
          await linkChecks.upsert({
            url: activity.mapsLink,
            source_type: 'activity_maps',
            source_id: activity.id!,
            http_status: checkResult.httpStatus,
            is_valid: checkResult.isValid,
            error_message: checkResult.errorMessage,
            response_time_ms: checkResult.responseTimeMs,
          });
          results.push({
            source: 'activity_maps',
            source_id: activity.id,
            ...checkResult,
          });
        }

        // Check review link
        if (activity.reviewLink) {
          const checkResult = await linkValidationService.checkLink(activity.reviewLink);
          await linkChecks.upsert({
            url: activity.reviewLink,
            source_type: 'activity_review',
            source_id: activity.id!,
            http_status: checkResult.httpStatus,
            is_valid: checkResult.isValid,
            error_message: checkResult.errorMessage,
            response_time_ms: checkResult.responseTimeMs,
          });
          results.push({
            source: 'activity_review',
            source_id: activity.id,
            ...checkResult,
          });
        }
      }

      const validCount = results.filter((r) => r.isValid).length;
      const invalidCount = results.filter((r) => !r.isValid).length;

      res.json({
        success: true,
        message: 'Link validation completed',
        data: {
          total: results.length,
          valid: validCount,
          invalid: invalidCount,
          results,
        },
      });
    } catch (error) {
      console.error('Error checking all links:', error);
      res.status(500).json({
        success: false,
        error: 'Failed to check links',
        message: error instanceof Error ? error.message : 'Unknown error',
      });
    }
  });

  /**
   * POST /api/link-validation/check-url
   * Manually check a single URL
   */
  router.post('/check-url', async (req: Request, res: Response) => {
    try {
      if (!req.body) {
        return res.status(400).json({
          success: false,
          error: 'Request body is required',
        });
      }

      const { url } = req.body;

      if (!url) {
        return res.status(400).json({
          success: false,
          error: 'URL is required',
        });
      }

      const checkResult = await linkValidationService.checkLink(url);

      res.json({
        success: true,
        data: checkResult,
      });
    } catch (error) {
      console.error('Error checking URL:', error);
      res.status(500).json({
        success: false,
        error: 'Failed to check URL',
        message: error instanceof Error ? error.message : 'Unknown error',
      });
    }
  });

  return router;
}

// Export default instance
export default createLinkValidationRouter();