Google maps integration
This commit is contained in:
@@ -48,8 +48,20 @@ router.get("/", async (req, res) => {
|
||||
order: [["createdAt", "DESC"]],
|
||||
});
|
||||
|
||||
// Round coordinates to 2 decimal places for map display while keeping precise values in database
|
||||
const itemsWithRoundedCoords = rows.map(item => {
|
||||
const itemData = item.toJSON();
|
||||
if (itemData.latitude !== null && itemData.latitude !== undefined) {
|
||||
itemData.latitude = Math.round(parseFloat(itemData.latitude) * 100) / 100;
|
||||
}
|
||||
if (itemData.longitude !== null && itemData.longitude !== undefined) {
|
||||
itemData.longitude = Math.round(parseFloat(itemData.longitude) * 100) / 100;
|
||||
}
|
||||
return itemData;
|
||||
});
|
||||
|
||||
res.json({
|
||||
items: rows,
|
||||
items: itemsWithRoundedCoords,
|
||||
totalPages: Math.ceil(count / limit),
|
||||
currentPage: parseInt(page),
|
||||
totalItems: count,
|
||||
@@ -134,7 +146,16 @@ router.get("/:id", async (req, res) => {
|
||||
return res.status(404).json({ error: "Item not found" });
|
||||
}
|
||||
|
||||
res.json(item);
|
||||
// Round coordinates to 2 decimal places for map display while keeping precise values in database
|
||||
const itemResponse = item.toJSON();
|
||||
if (itemResponse.latitude !== null && itemResponse.latitude !== undefined) {
|
||||
itemResponse.latitude = Math.round(parseFloat(itemResponse.latitude) * 100) / 100;
|
||||
}
|
||||
if (itemResponse.longitude !== null && itemResponse.longitude !== undefined) {
|
||||
itemResponse.longitude = Math.round(parseFloat(itemResponse.longitude) * 100) / 100;
|
||||
}
|
||||
|
||||
res.json(itemResponse);
|
||||
} catch (error) {
|
||||
res.status(500).json({ error: error.message });
|
||||
}
|
||||
|
||||
198
backend/routes/maps.js
Normal file
198
backend/routes/maps.js
Normal file
@@ -0,0 +1,198 @@
|
||||
const express = require("express");
|
||||
const router = express.Router();
|
||||
const { authenticateToken } = require("../middleware/auth");
|
||||
const rateLimiter = require("../middleware/rateLimiter");
|
||||
const googleMapsService = require("../services/googleMapsService");
|
||||
|
||||
// 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) => {
|
||||
console.error("Maps service error:", error.message);
|
||||
|
||||
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)
|
||||
console.log(
|
||||
`Places Autocomplete: user=${
|
||||
req.user?.id || "anonymous"
|
||||
}, query_length=${input.length}, results=${
|
||||
result.predictions?.length || 0
|
||||
}`
|
||||
);
|
||||
|
||||
res.json(result);
|
||||
} catch (error) {
|
||||
handleServiceError(error, res);
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
/**
|
||||
* 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
|
||||
console.log(
|
||||
`Place Details: user=${
|
||||
req.user?.id || "anonymous"
|
||||
}, placeId=${placeId.substring(0, 10)}...`
|
||||
);
|
||||
|
||||
res.json(result);
|
||||
} catch (error) {
|
||||
handleServiceError(error, res);
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
/**
|
||||
* 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
|
||||
console.log(
|
||||
`Geocoding: user=${req.user?.id || "anonymous"}, address_length=${
|
||||
address.length
|
||||
}`
|
||||
);
|
||||
|
||||
res.json(result);
|
||||
} catch (error) {
|
||||
handleServiceError(error, res);
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
/**
|
||||
* 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;
|
||||
@@ -239,7 +239,13 @@ router.put("/:id/status", authenticateToken, async (req, res) => {
|
||||
{
|
||||
model: User,
|
||||
as: "renter",
|
||||
attributes: ["id", "username", "firstName", "lastName", "stripeCustomerId"],
|
||||
attributes: [
|
||||
"id",
|
||||
"username",
|
||||
"firstName",
|
||||
"lastName",
|
||||
"stripeCustomerId",
|
||||
],
|
||||
},
|
||||
],
|
||||
});
|
||||
@@ -253,18 +259,26 @@ router.put("/:id/status", authenticateToken, async (req, res) => {
|
||||
}
|
||||
|
||||
// If owner is approving a pending rental, charge the stored payment method
|
||||
if (status === "confirmed" && rental.status === "pending" && rental.ownerId === req.user.id) {
|
||||
if (
|
||||
status === "confirmed" &&
|
||||
rental.status === "pending" &&
|
||||
rental.ownerId === req.user.id
|
||||
) {
|
||||
if (!rental.stripePaymentMethodId) {
|
||||
return res.status(400).json({ error: "No payment method found for this rental" });
|
||||
return res
|
||||
.status(400)
|
||||
.json({ error: "No payment method found for this rental" });
|
||||
}
|
||||
|
||||
try {
|
||||
// Import StripeService to process the payment
|
||||
const StripeService = require("../services/stripeService");
|
||||
|
||||
|
||||
// Check if renter has a stripe customer ID
|
||||
if (!rental.renter.stripeCustomerId) {
|
||||
return res.status(400).json({ error: "Renter does not have a Stripe customer account" });
|
||||
return res
|
||||
.status(400)
|
||||
.json({ error: "Renter does not have a Stripe customer account" });
|
||||
}
|
||||
|
||||
// Create payment intent and charge the stored payment method
|
||||
@@ -308,9 +322,9 @@ router.put("/:id/status", authenticateToken, async (req, res) => {
|
||||
} catch (paymentError) {
|
||||
console.error("Payment failed during approval:", paymentError);
|
||||
// Keep rental as pending, but inform of payment failure
|
||||
return res.status(400).json({
|
||||
error: "Payment failed during approval",
|
||||
details: paymentError.message
|
||||
return res.status(400).json({
|
||||
error: "Payment failed during approval",
|
||||
details: paymentError.message,
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -430,7 +444,6 @@ router.post("/:id/review-item", authenticateToken, async (req, res) => {
|
||||
// Mark rental as completed (owner only)
|
||||
router.post("/:id/mark-completed", authenticateToken, async (req, res) => {
|
||||
try {
|
||||
console.log("Mark completed endpoint hit for rental ID:", req.params.id);
|
||||
const rental = await Rental.findByPk(req.params.id);
|
||||
|
||||
if (!rental) {
|
||||
|
||||
Reference in New Issue
Block a user