200 lines
4.9 KiB
JavaScript
200 lines
4.9 KiB
JavaScript
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.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.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.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;
|