const express = require("express"); const router = express.Router(); const { authenticateToken } = require("../middleware/auth"); const rateLimiter = require("../middleware/rateLimiter"); const googleMapsService = require("../services/googleMapsService"); const logger = require("../utils/logger"); // Input validation middleware const validateInput = (req, res, next) => { // Basic input sanitization if (req.body.input) { req.body.input = req.body.input.toString().trim(); // Prevent extremely long inputs if (req.body.input.length > 500) { return res.status(400).json({ error: "Input too long" }); } } if (req.body.placeId) { req.body.placeId = req.body.placeId.toString().trim(); // Basic place ID validation if (!/^[A-Za-z0-9_-]+$/.test(req.body.placeId)) { return res.status(400).json({ error: "Invalid place ID format" }); } } if (req.body.address) { req.body.address = req.body.address.toString().trim(); if (req.body.address.length > 500) { return res.status(400).json({ error: "Address too long" }); } } next(); }; // Error handling middleware const handleServiceError = (error, res, req) => { const reqLogger = logger.withRequestId(req?.id); reqLogger.error("Maps service error", { error: error.message, stack: error.stack }); if (error.message.includes("API key not configured")) { return res.status(503).json({ error: "Maps service temporarily unavailable", details: "Configuration issue", }); } if (error.message.includes("quota exceeded")) { return res.status(429).json({ error: "Service temporarily unavailable due to high demand", details: "Please try again later", }); } return res.status(500).json({ error: "Failed to process request", details: error.message, }); }; /** * POST /api/maps/places/autocomplete * Proxy for Google Places Autocomplete API */ router.post( "/places/autocomplete", authenticateToken, rateLimiter.burstProtection, rateLimiter.placesAutocomplete, validateInput, async (req, res) => { try { const { input, types, componentRestrictions, sessionToken } = req.body; if (!input || input.length < 2) { return res.json({ predictions: [] }); } const options = { types: types || ["address"], componentRestrictions, sessionToken, }; const result = await googleMapsService.getPlacesAutocomplete( input, options ); // Log request for monitoring (without sensitive data) const reqLogger = logger.withRequestId(req.id); reqLogger.info("Places Autocomplete request", { userId: req.user?.id || "anonymous", queryLength: input.length, resultsCount: result.predictions?.length || 0 }); res.json(result); } catch (error) { handleServiceError(error, res, req); } } ); /** * POST /api/maps/places/details * Proxy for Google Places Details API */ router.post( "/places/details", authenticateToken, rateLimiter.burstProtection, rateLimiter.placeDetails, validateInput, async (req, res) => { try { const { placeId, sessionToken } = req.body; if (!placeId) { return res.status(400).json({ error: "Place ID is required" }); } const options = { sessionToken, }; const result = await googleMapsService.getPlaceDetails(placeId, options); // Log request for monitoring const reqLogger = logger.withRequestId(req.id); reqLogger.info("Place Details request", { userId: req.user?.id || "anonymous", placeIdPrefix: placeId.substring(0, 10) + "..." }); res.json(result); } catch (error) { handleServiceError(error, res, req); } } ); /** * POST /api/maps/geocode * Proxy for Google Geocoding API */ router.post( "/geocode", authenticateToken, rateLimiter.burstProtection, rateLimiter.geocoding, validateInput, async (req, res) => { try { const { address, componentRestrictions } = req.body; if (!address) { return res.status(400).json({ error: "Address is required" }); } const options = { componentRestrictions, }; const result = await googleMapsService.geocodeAddress(address, options); // Log request for monitoring const reqLogger = logger.withRequestId(req.id); reqLogger.info("Geocoding request", { userId: req.user?.id || "anonymous", addressLength: address.length }); res.json(result); } catch (error) { handleServiceError(error, res, req); } } ); /** * GET /api/maps/health * Health check endpoint for Maps service */ router.get("/health", (req, res) => { const isConfigured = googleMapsService.isConfigured(); res.status(isConfigured ? 200 : 503).json({ status: isConfigured ? "healthy" : "unavailable", service: "Google Maps API Proxy", timestamp: new Date().toISOString(), configuration: { apiKeyConfigured: isConfigured, }, }); }); module.exports = router;