Google maps integration
This commit is contained in:
119
backend/middleware/rateLimiter.js
Normal file
119
backend/middleware/rateLimiter.js
Normal file
@@ -0,0 +1,119 @@
|
||||
const rateLimit = require("express-rate-limit");
|
||||
|
||||
// General rate limiter for Maps API endpoints
|
||||
const createMapsRateLimiter = (windowMs, max, message) => {
|
||||
return rateLimit({
|
||||
windowMs, // time window in milliseconds
|
||||
max, // limit each IP/user to max requests per windowMs
|
||||
message: {
|
||||
error: message,
|
||||
retryAfter: Math.ceil(windowMs / 1000), // seconds
|
||||
},
|
||||
standardHeaders: true, // Return rate limit info in the `RateLimit-*` headers
|
||||
legacyHeaders: false, // Disable the `X-RateLimit-*` headers
|
||||
// Use user ID if available, otherwise fall back to IPv6-safe IP handling
|
||||
keyGenerator: (req) => {
|
||||
if (req.user?.id) {
|
||||
return `user:${req.user.id}`;
|
||||
}
|
||||
// Use the built-in IP key generator which properly handles IPv6
|
||||
return rateLimit.defaultKeyGenerator(req);
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
// Specific rate limiters for different endpoints
|
||||
const rateLimiters = {
|
||||
// Places Autocomplete - allow more requests since users type frequently
|
||||
placesAutocomplete: createMapsRateLimiter(
|
||||
60 * 1000, // 1 minute window
|
||||
30, // 30 requests per minute per user/IP
|
||||
"Too many autocomplete requests. Please slow down."
|
||||
),
|
||||
|
||||
// Place Details - moderate limit since each selection triggers this
|
||||
placeDetails: createMapsRateLimiter(
|
||||
60 * 1000, // 1 minute window
|
||||
20, // 20 requests per minute per user/IP
|
||||
"Too many place detail requests. Please slow down."
|
||||
),
|
||||
|
||||
// Geocoding - lower limit since this is typically used less frequently
|
||||
geocoding: createMapsRateLimiter(
|
||||
60 * 1000, // 1 minute window
|
||||
10, // 10 requests per minute per user/IP
|
||||
"Too many geocoding requests. Please slow down."
|
||||
),
|
||||
};
|
||||
|
||||
// Enhanced rate limiter with user-specific limits
|
||||
const createUserBasedRateLimiter = (windowMs, max, message) => {
|
||||
const store = new Map(); // Simple in-memory store (use Redis in production)
|
||||
|
||||
return (req, res, next) => {
|
||||
const key = req.user?.id
|
||||
? `user:${req.user.id}`
|
||||
: rateLimit.defaultKeyGenerator(req);
|
||||
const now = Date.now();
|
||||
const windowStart = now - windowMs;
|
||||
|
||||
// Clean up old entries
|
||||
for (const [k, data] of store.entries()) {
|
||||
if (data.windowStart < windowStart) {
|
||||
store.delete(k);
|
||||
}
|
||||
}
|
||||
|
||||
// Get or create user's request data
|
||||
let userData = store.get(key);
|
||||
if (!userData || userData.windowStart < windowStart) {
|
||||
userData = {
|
||||
count: 0,
|
||||
windowStart: now,
|
||||
resetTime: now + windowMs,
|
||||
};
|
||||
}
|
||||
|
||||
// Check if limit exceeded
|
||||
if (userData.count >= max) {
|
||||
return res.status(429).json({
|
||||
error: message,
|
||||
retryAfter: Math.ceil((userData.resetTime - now) / 1000),
|
||||
});
|
||||
}
|
||||
|
||||
// Increment counter and store
|
||||
userData.count++;
|
||||
store.set(key, userData);
|
||||
|
||||
// Add headers
|
||||
res.set({
|
||||
"RateLimit-Limit": max,
|
||||
"RateLimit-Remaining": Math.max(0, max - userData.count),
|
||||
"RateLimit-Reset": new Date(userData.resetTime).toISOString(),
|
||||
});
|
||||
|
||||
next();
|
||||
};
|
||||
};
|
||||
|
||||
// Burst protection for expensive operations
|
||||
const burstProtection = createUserBasedRateLimiter(
|
||||
10 * 1000, // 10 seconds
|
||||
5, // 5 requests per 10 seconds
|
||||
"Too many requests in a short period. Please slow down."
|
||||
);
|
||||
|
||||
module.exports = {
|
||||
// Individual rate limiters
|
||||
placesAutocomplete: rateLimiters.placesAutocomplete,
|
||||
placeDetails: rateLimiters.placeDetails,
|
||||
geocoding: rateLimiters.geocoding,
|
||||
|
||||
// Burst protection
|
||||
burstProtection,
|
||||
|
||||
// Utility functions
|
||||
createMapsRateLimiter,
|
||||
createUserBasedRateLimiter,
|
||||
};
|
||||
275
backend/package-lock.json
generated
275
backend/package-lock.json
generated
@@ -9,11 +9,13 @@
|
||||
"version": "1.0.0",
|
||||
"license": "ISC",
|
||||
"dependencies": {
|
||||
"@googlemaps/google-maps-services-js": "^3.4.2",
|
||||
"bcryptjs": "^3.0.2",
|
||||
"body-parser": "^2.2.0",
|
||||
"cors": "^2.8.5",
|
||||
"dotenv": "^17.2.0",
|
||||
"express": "^5.1.0",
|
||||
"express-rate-limit": "^8.1.0",
|
||||
"jsonwebtoken": "^9.0.2",
|
||||
"multer": "^2.0.2",
|
||||
"node-cron": "^3.0.3",
|
||||
@@ -27,6 +29,28 @@
|
||||
"nodemon": "^3.1.10"
|
||||
}
|
||||
},
|
||||
"node_modules/@googlemaps/google-maps-services-js": {
|
||||
"version": "3.4.2",
|
||||
"resolved": "https://registry.npmjs.org/@googlemaps/google-maps-services-js/-/google-maps-services-js-3.4.2.tgz",
|
||||
"integrity": "sha512-QjxiJSt8woyPaaQIUUDNL1nRfCEXTiv8KfJSNq/YzKEUVXABT75GLG+Zo267UwOcq70n8OsZbaro5VrY962mJg==",
|
||||
"license": "Apache-2.0",
|
||||
"dependencies": {
|
||||
"@googlemaps/url-signature": "^1.0.4",
|
||||
"agentkeepalive": "^4.1.0",
|
||||
"axios": "^1.5.1",
|
||||
"query-string": "<8.x",
|
||||
"retry-axios": "<3.x"
|
||||
}
|
||||
},
|
||||
"node_modules/@googlemaps/url-signature": {
|
||||
"version": "1.0.40",
|
||||
"resolved": "https://registry.npmjs.org/@googlemaps/url-signature/-/url-signature-1.0.40.tgz",
|
||||
"integrity": "sha512-Gme3JxGZWQ4NVpATajSpS2/inQzhUxRvr/FK6IFpcC7AHOAmx8blI0y1/Qi2jqil+WoQ3TkEqq/MaKVtuV68RQ==",
|
||||
"license": "Apache-2.0",
|
||||
"dependencies": {
|
||||
"crypto-js": "^4.2.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@isaacs/cliui": {
|
||||
"version": "8.0.2",
|
||||
"resolved": "https://registry.npmjs.org/@isaacs/cliui/-/cliui-8.0.2.tgz",
|
||||
@@ -103,6 +127,18 @@
|
||||
"node": ">= 0.6"
|
||||
}
|
||||
},
|
||||
"node_modules/agentkeepalive": {
|
||||
"version": "4.6.0",
|
||||
"resolved": "https://registry.npmjs.org/agentkeepalive/-/agentkeepalive-4.6.0.tgz",
|
||||
"integrity": "sha512-kja8j7PjmncONqaTsB8fQ+wE2mSU2DJ9D4XKoJ5PFWIdRMa6SLSN1ff4mOr4jCbfRSsxR4keIiySJU0N9T5hIQ==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"humanize-ms": "^1.2.1"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 8.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/ansi-regex": {
|
||||
"version": "6.1.0",
|
||||
"resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.1.0.tgz",
|
||||
@@ -144,6 +180,12 @@
|
||||
"integrity": "sha512-klpgFSWLW1ZEs8svjfb7g4qWY0YS5imI82dTg+QahUvJ8YqAY0P10Uk8tTyh9ZGuYEZEMaeJYCF5BFuX552hsw==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/asynckit": {
|
||||
"version": "0.4.0",
|
||||
"resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz",
|
||||
"integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/at-least-node": {
|
||||
"version": "1.0.0",
|
||||
"resolved": "https://registry.npmjs.org/at-least-node/-/at-least-node-1.0.0.tgz",
|
||||
@@ -152,6 +194,17 @@
|
||||
"node": ">= 4.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/axios": {
|
||||
"version": "1.11.0",
|
||||
"resolved": "https://registry.npmjs.org/axios/-/axios-1.11.0.tgz",
|
||||
"integrity": "sha512-1Lx3WLFQWm3ooKDYZD1eXmoGO9fxYQjrycfHFC8P0sCfQVXyROp0p9PFWBehewBOdCwHc+f/b8I0fMto5eSfwA==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"follow-redirects": "^1.15.6",
|
||||
"form-data": "^4.0.4",
|
||||
"proxy-from-env": "^1.1.0"
|
||||
}
|
||||
},
|
||||
"node_modules/balanced-match": {
|
||||
"version": "1.0.2",
|
||||
"resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz",
|
||||
@@ -395,6 +448,18 @@
|
||||
"resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz",
|
||||
"integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA=="
|
||||
},
|
||||
"node_modules/combined-stream": {
|
||||
"version": "1.0.8",
|
||||
"resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz",
|
||||
"integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"delayed-stream": "~1.0.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 0.8"
|
||||
}
|
||||
},
|
||||
"node_modules/commander": {
|
||||
"version": "10.0.1",
|
||||
"resolved": "https://registry.npmjs.org/commander/-/commander-10.0.1.tgz",
|
||||
@@ -493,6 +558,12 @@
|
||||
"node": ">= 8"
|
||||
}
|
||||
},
|
||||
"node_modules/crypto-js": {
|
||||
"version": "4.2.0",
|
||||
"resolved": "https://registry.npmjs.org/crypto-js/-/crypto-js-4.2.0.tgz",
|
||||
"integrity": "sha512-KALDyEYgpY+Rlob/iriUtjV6d5Eq+Y191A5g4UqLAi8CyGP9N1+FdVbkc1SxKc2r4YAYqG8JzO2KGL+AizD70Q==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/debug": {
|
||||
"version": "4.4.1",
|
||||
"resolved": "https://registry.npmjs.org/debug/-/debug-4.4.1.tgz",
|
||||
@@ -509,6 +580,24 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/decode-uri-component": {
|
||||
"version": "0.2.2",
|
||||
"resolved": "https://registry.npmjs.org/decode-uri-component/-/decode-uri-component-0.2.2.tgz",
|
||||
"integrity": "sha512-FqUYQ+8o158GyGTrMFJms9qh3CqTKvAqgqsTnkLI8sKu0028orqBhxNMFkFen0zGyg6epACD32pjVk58ngIErQ==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=0.10"
|
||||
}
|
||||
},
|
||||
"node_modules/delayed-stream": {
|
||||
"version": "1.0.0",
|
||||
"resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz",
|
||||
"integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=0.4.0"
|
||||
}
|
||||
},
|
||||
"node_modules/depd": {
|
||||
"version": "2.0.0",
|
||||
"resolved": "https://registry.npmjs.org/depd/-/depd-2.0.0.tgz",
|
||||
@@ -621,6 +710,21 @@
|
||||
"node": ">= 0.4"
|
||||
}
|
||||
},
|
||||
"node_modules/es-set-tostringtag": {
|
||||
"version": "2.1.0",
|
||||
"resolved": "https://registry.npmjs.org/es-set-tostringtag/-/es-set-tostringtag-2.1.0.tgz",
|
||||
"integrity": "sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"es-errors": "^1.3.0",
|
||||
"get-intrinsic": "^1.2.6",
|
||||
"has-tostringtag": "^1.0.2",
|
||||
"hasown": "^2.0.2"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 0.4"
|
||||
}
|
||||
},
|
||||
"node_modules/escalade": {
|
||||
"version": "3.2.0",
|
||||
"resolved": "https://registry.npmjs.org/escalade/-/escalade-3.2.0.tgz",
|
||||
@@ -683,6 +787,24 @@
|
||||
"url": "https://opencollective.com/express"
|
||||
}
|
||||
},
|
||||
"node_modules/express-rate-limit": {
|
||||
"version": "8.1.0",
|
||||
"resolved": "https://registry.npmjs.org/express-rate-limit/-/express-rate-limit-8.1.0.tgz",
|
||||
"integrity": "sha512-4nLnATuKupnmwqiJc27b4dCFmB/T60ExgmtDD7waf4LdrbJ8CPZzZRHYErDYNhoz+ql8fUdYwM/opf90PoPAQA==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"ip-address": "10.0.1"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 16"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/express-rate-limit"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"express": ">= 4.11"
|
||||
}
|
||||
},
|
||||
"node_modules/fill-range": {
|
||||
"version": "7.1.1",
|
||||
"resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.1.1.tgz",
|
||||
@@ -695,6 +817,15 @@
|
||||
"node": ">=8"
|
||||
}
|
||||
},
|
||||
"node_modules/filter-obj": {
|
||||
"version": "1.1.0",
|
||||
"resolved": "https://registry.npmjs.org/filter-obj/-/filter-obj-1.1.0.tgz",
|
||||
"integrity": "sha512-8rXg1ZnX7xzy2NGDVkBVaAy+lSlPNwad13BtgSlLuxfIslyt5Vg64U7tFcCt4WS1R0hvtnQybT/IyCkGZ3DpXQ==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=0.10.0"
|
||||
}
|
||||
},
|
||||
"node_modules/finalhandler": {
|
||||
"version": "2.1.0",
|
||||
"resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-2.1.0.tgz",
|
||||
@@ -711,6 +842,26 @@
|
||||
"node": ">= 0.8"
|
||||
}
|
||||
},
|
||||
"node_modules/follow-redirects": {
|
||||
"version": "1.15.11",
|
||||
"resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.11.tgz",
|
||||
"integrity": "sha512-deG2P0JfjrTxl50XGCDyfI97ZGVCxIpfKYmfyrQ54n5FO/0gfIES8C/Psl6kWVDolizcaaxZJnTS0QSMxvnsBQ==",
|
||||
"funding": [
|
||||
{
|
||||
"type": "individual",
|
||||
"url": "https://github.com/sponsors/RubenVerborgh"
|
||||
}
|
||||
],
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=4.0"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"debug": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/foreground-child": {
|
||||
"version": "3.3.1",
|
||||
"resolved": "https://registry.npmjs.org/foreground-child/-/foreground-child-3.3.1.tgz",
|
||||
@@ -726,6 +877,43 @@
|
||||
"url": "https://github.com/sponsors/isaacs"
|
||||
}
|
||||
},
|
||||
"node_modules/form-data": {
|
||||
"version": "4.0.4",
|
||||
"resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.4.tgz",
|
||||
"integrity": "sha512-KrGhL9Q4zjj0kiUt5OO4Mr/A/jlI2jDYs5eHBpYHPcBEVSiipAvn2Ko2HnPe20rmcuuvMHNdZFp+4IlGTMF0Ow==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"asynckit": "^0.4.0",
|
||||
"combined-stream": "^1.0.8",
|
||||
"es-set-tostringtag": "^2.1.0",
|
||||
"hasown": "^2.0.2",
|
||||
"mime-types": "^2.1.12"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 6"
|
||||
}
|
||||
},
|
||||
"node_modules/form-data/node_modules/mime-db": {
|
||||
"version": "1.52.0",
|
||||
"resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz",
|
||||
"integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">= 0.6"
|
||||
}
|
||||
},
|
||||
"node_modules/form-data/node_modules/mime-types": {
|
||||
"version": "2.1.35",
|
||||
"resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz",
|
||||
"integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"mime-db": "1.52.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 0.6"
|
||||
}
|
||||
},
|
||||
"node_modules/forwarded": {
|
||||
"version": "0.2.0",
|
||||
"resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.2.0.tgz",
|
||||
@@ -902,6 +1090,21 @@
|
||||
"url": "https://github.com/sponsors/ljharb"
|
||||
}
|
||||
},
|
||||
"node_modules/has-tostringtag": {
|
||||
"version": "1.0.2",
|
||||
"resolved": "https://registry.npmjs.org/has-tostringtag/-/has-tostringtag-1.0.2.tgz",
|
||||
"integrity": "sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"has-symbols": "^1.0.3"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 0.4"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/ljharb"
|
||||
}
|
||||
},
|
||||
"node_modules/hasown": {
|
||||
"version": "2.0.2",
|
||||
"resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz",
|
||||
@@ -936,6 +1139,15 @@
|
||||
"node": ">= 0.8"
|
||||
}
|
||||
},
|
||||
"node_modules/humanize-ms": {
|
||||
"version": "1.2.1",
|
||||
"resolved": "https://registry.npmjs.org/humanize-ms/-/humanize-ms-1.2.1.tgz",
|
||||
"integrity": "sha512-Fl70vYtsAFb/C06PTS9dZBo7ihau+Tu/DNCk/OyHhea07S+aeMWpFFkUaXRa8fI+ScZbEI8dfSxwY7gxZ9SAVQ==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"ms": "^2.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/iconv-lite": {
|
||||
"version": "0.6.3",
|
||||
"resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.6.3.tgz",
|
||||
@@ -971,6 +1183,15 @@
|
||||
"resolved": "https://registry.npmjs.org/ini/-/ini-1.3.8.tgz",
|
||||
"integrity": "sha512-JV/yugV2uzW5iMRSiZAyDtQd+nxtUnjeLt0acNdw98kKLrvuRVyB80tsREOE7yvGVgalhZ6RNXCmEHkUKBKxew=="
|
||||
},
|
||||
"node_modules/ip-address": {
|
||||
"version": "10.0.1",
|
||||
"resolved": "https://registry.npmjs.org/ip-address/-/ip-address-10.0.1.tgz",
|
||||
"integrity": "sha512-NWv9YLW4PoW2B7xtzaS3NCot75m6nK7Icdv0o3lfMceJVRfSoQwqD4wEH5rLwoKJwUiZ/rfpiVBhnaF0FK4HoA==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">= 12"
|
||||
}
|
||||
},
|
||||
"node_modules/ipaddr.js": {
|
||||
"version": "1.9.1",
|
||||
"resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-1.9.1.tgz",
|
||||
@@ -1704,6 +1925,12 @@
|
||||
"node": ">= 0.10"
|
||||
}
|
||||
},
|
||||
"node_modules/proxy-from-env": {
|
||||
"version": "1.1.0",
|
||||
"resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-1.1.0.tgz",
|
||||
"integrity": "sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/pstree.remy": {
|
||||
"version": "1.1.8",
|
||||
"resolved": "https://registry.npmjs.org/pstree.remy/-/pstree.remy-1.1.8.tgz",
|
||||
@@ -1724,6 +1951,24 @@
|
||||
"url": "https://github.com/sponsors/ljharb"
|
||||
}
|
||||
},
|
||||
"node_modules/query-string": {
|
||||
"version": "7.1.3",
|
||||
"resolved": "https://registry.npmjs.org/query-string/-/query-string-7.1.3.tgz",
|
||||
"integrity": "sha512-hh2WYhq4fi8+b+/2Kg9CEge4fDPvHS534aOOvOZeQ3+Vf2mCFsaFBYj0i+iXcAq6I9Vzp5fjMFBlONvayDC1qg==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"decode-uri-component": "^0.2.2",
|
||||
"filter-obj": "^1.1.0",
|
||||
"split-on-first": "^1.0.0",
|
||||
"strict-uri-encode": "^2.0.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=6"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/sindresorhus"
|
||||
}
|
||||
},
|
||||
"node_modules/range-parser": {
|
||||
"version": "1.2.1",
|
||||
"resolved": "https://registry.npmjs.org/range-parser/-/range-parser-1.2.1.tgz",
|
||||
@@ -1804,6 +2049,18 @@
|
||||
"resolved": "https://registry.npmjs.org/retry-as-promised/-/retry-as-promised-7.1.1.tgz",
|
||||
"integrity": "sha512-hMD7odLOt3LkTjcif8aRZqi/hybjpLNgSk5oF5FCowfCjok6LukpN2bDX7R5wDmbgBQFn7YoBxSagmtXHaJYJw=="
|
||||
},
|
||||
"node_modules/retry-axios": {
|
||||
"version": "2.6.0",
|
||||
"resolved": "https://registry.npmjs.org/retry-axios/-/retry-axios-2.6.0.tgz",
|
||||
"integrity": "sha512-pOLi+Gdll3JekwuFjXO3fTq+L9lzMQGcSq7M5gIjExcl3Gu1hd4XXuf5o3+LuSBsaULQH7DiNbsqPd1chVpQGQ==",
|
||||
"license": "Apache-2.0",
|
||||
"engines": {
|
||||
"node": ">=10.7.0"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"axios": "*"
|
||||
}
|
||||
},
|
||||
"node_modules/router": {
|
||||
"version": "2.2.0",
|
||||
"resolved": "https://registry.npmjs.org/router/-/router-2.2.0.tgz",
|
||||
@@ -2103,6 +2360,15 @@
|
||||
"node": ">=10"
|
||||
}
|
||||
},
|
||||
"node_modules/split-on-first": {
|
||||
"version": "1.1.0",
|
||||
"resolved": "https://registry.npmjs.org/split-on-first/-/split-on-first-1.1.0.tgz",
|
||||
"integrity": "sha512-43ZssAJaMusuKWL8sKUBQXHWOpq8d6CfN/u1p4gUzfJkM05C8rxTmYrkIPTXapZpORA6LkkzcUulJ8FqA7Uudw==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=6"
|
||||
}
|
||||
},
|
||||
"node_modules/split2": {
|
||||
"version": "4.2.0",
|
||||
"resolved": "https://registry.npmjs.org/split2/-/split2-4.2.0.tgz",
|
||||
@@ -2127,6 +2393,15 @@
|
||||
"node": ">=10.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/strict-uri-encode": {
|
||||
"version": "2.0.0",
|
||||
"resolved": "https://registry.npmjs.org/strict-uri-encode/-/strict-uri-encode-2.0.0.tgz",
|
||||
"integrity": "sha512-QwiXZgpRcKkhTj2Scnn++4PKtWsH0kpzZ62L2R6c/LUVYv7hVnZqcg2+sMuT6R7Jusu1vviK/MFsu6kNJfWlEQ==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=4"
|
||||
}
|
||||
},
|
||||
"node_modules/string_decoder": {
|
||||
"version": "1.3.0",
|
||||
"resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.3.0.tgz",
|
||||
|
||||
@@ -16,11 +16,13 @@
|
||||
"author": "",
|
||||
"license": "ISC",
|
||||
"dependencies": {
|
||||
"@googlemaps/google-maps-services-js": "^3.4.2",
|
||||
"bcryptjs": "^3.0.2",
|
||||
"body-parser": "^2.2.0",
|
||||
"cors": "^2.8.5",
|
||||
"dotenv": "^17.2.0",
|
||||
"express": "^5.1.0",
|
||||
"express-rate-limit": "^8.1.0",
|
||||
"jsonwebtoken": "^9.0.2",
|
||||
"multer": "^2.0.2",
|
||||
"node-cron": "^3.0.3",
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -20,6 +20,7 @@ const messageRoutes = require("./routes/messages");
|
||||
const betaRoutes = require("./routes/beta");
|
||||
const itemRequestRoutes = require("./routes/itemRequests");
|
||||
const stripeRoutes = require("./routes/stripe");
|
||||
const mapsRoutes = require("./routes/maps");
|
||||
|
||||
const PayoutProcessor = require("./jobs/payoutProcessor");
|
||||
|
||||
@@ -43,6 +44,7 @@ app.use("/api/rentals", rentalRoutes);
|
||||
app.use("/api/messages", messageRoutes);
|
||||
app.use("/api/item-requests", itemRequestRoutes);
|
||||
app.use("/api/stripe", stripeRoutes);
|
||||
app.use("/api/maps", mapsRoutes);
|
||||
|
||||
app.get("/", (req, res) => {
|
||||
res.json({ message: "CommunityRentals.App API is running!" });
|
||||
|
||||
241
backend/services/googleMapsService.js
Normal file
241
backend/services/googleMapsService.js
Normal file
@@ -0,0 +1,241 @@
|
||||
const { Client } = require('@googlemaps/google-maps-services-js');
|
||||
|
||||
class GoogleMapsService {
|
||||
constructor() {
|
||||
this.client = new Client({});
|
||||
this.apiKey = process.env.GOOGLE_MAPS_API_KEY;
|
||||
|
||||
if (!this.apiKey) {
|
||||
console.error('❌ Google Maps API key not configured in environment variables');
|
||||
} else {
|
||||
console.log('✅ Google Maps service initialized');
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get autocomplete predictions for places
|
||||
*/
|
||||
async getPlacesAutocomplete(input, options = {}) {
|
||||
if (!this.apiKey) {
|
||||
throw new Error('Google Maps API key not configured');
|
||||
}
|
||||
|
||||
if (!input || input.trim().length < 2) {
|
||||
return { predictions: [] };
|
||||
}
|
||||
|
||||
try {
|
||||
const params = {
|
||||
key: this.apiKey,
|
||||
input: input.trim(),
|
||||
types: options.types || 'address',
|
||||
language: options.language || 'en',
|
||||
...options
|
||||
};
|
||||
|
||||
// Add session token if provided
|
||||
if (options.sessionToken) {
|
||||
params.sessiontoken = options.sessionToken;
|
||||
}
|
||||
|
||||
// Add component restrictions (e.g., country)
|
||||
if (options.componentRestrictions) {
|
||||
params.components = Object.entries(options.componentRestrictions)
|
||||
.map(([key, value]) => `${key}:${value}`)
|
||||
.join('|');
|
||||
}
|
||||
|
||||
const response = await this.client.placeAutocomplete({
|
||||
params,
|
||||
timeout: 5000
|
||||
});
|
||||
|
||||
if (response.data.status === 'OK') {
|
||||
return {
|
||||
predictions: response.data.predictions.map(prediction => ({
|
||||
placeId: prediction.place_id,
|
||||
description: prediction.description,
|
||||
types: prediction.types,
|
||||
mainText: prediction.structured_formatting.main_text,
|
||||
secondaryText: prediction.structured_formatting.secondary_text || ''
|
||||
}))
|
||||
};
|
||||
} else {
|
||||
console.error('Places Autocomplete API error:', response.data.status, response.data.error_message);
|
||||
return {
|
||||
predictions: [],
|
||||
error: this.getErrorMessage(response.data.status),
|
||||
status: response.data.status
|
||||
};
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Places Autocomplete service error:', error.message);
|
||||
throw new Error('Failed to fetch place predictions');
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get detailed information about a place
|
||||
*/
|
||||
async getPlaceDetails(placeId, options = {}) {
|
||||
if (!this.apiKey) {
|
||||
throw new Error('Google Maps API key not configured');
|
||||
}
|
||||
|
||||
if (!placeId) {
|
||||
throw new Error('Place ID is required');
|
||||
}
|
||||
|
||||
try {
|
||||
const params = {
|
||||
key: this.apiKey,
|
||||
place_id: placeId,
|
||||
fields: [
|
||||
'address_components',
|
||||
'formatted_address',
|
||||
'geometry',
|
||||
'place_id'
|
||||
],
|
||||
language: options.language || 'en'
|
||||
};
|
||||
|
||||
// Add session token if provided
|
||||
if (options.sessionToken) {
|
||||
params.sessiontoken = options.sessionToken;
|
||||
}
|
||||
|
||||
const response = await this.client.placeDetails({
|
||||
params,
|
||||
timeout: 5000
|
||||
});
|
||||
|
||||
if (response.data.status === 'OK' && response.data.result) {
|
||||
const place = response.data.result;
|
||||
const addressComponents = {};
|
||||
|
||||
// Parse address components
|
||||
if (place.address_components) {
|
||||
place.address_components.forEach(component => {
|
||||
const types = component.types;
|
||||
|
||||
if (types.includes('street_number')) {
|
||||
addressComponents.streetNumber = component.long_name;
|
||||
} else if (types.includes('route')) {
|
||||
addressComponents.route = component.long_name;
|
||||
} else if (types.includes('locality')) {
|
||||
addressComponents.locality = component.long_name;
|
||||
} else if (types.includes('administrative_area_level_1')) {
|
||||
addressComponents.administrativeAreaLevel1 = component.short_name;
|
||||
addressComponents.administrativeAreaLevel1Long = component.long_name;
|
||||
} else if (types.includes('postal_code')) {
|
||||
addressComponents.postalCode = component.long_name;
|
||||
} else if (types.includes('country')) {
|
||||
addressComponents.country = component.short_name;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
return {
|
||||
placeId: place.place_id,
|
||||
formattedAddress: place.formatted_address,
|
||||
addressComponents,
|
||||
geometry: {
|
||||
latitude: place.geometry?.location?.lat || 0,
|
||||
longitude: place.geometry?.location?.lng || 0
|
||||
}
|
||||
};
|
||||
} else {
|
||||
console.error('Place Details API error:', response.data.status, response.data.error_message);
|
||||
throw new Error(this.getErrorMessage(response.data.status));
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Place Details service error:', error.message);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Geocode an address to get coordinates
|
||||
*/
|
||||
async geocodeAddress(address, options = {}) {
|
||||
if (!this.apiKey) {
|
||||
throw new Error('Google Maps API key not configured');
|
||||
}
|
||||
|
||||
if (!address || !address.trim()) {
|
||||
throw new Error('Address is required for geocoding');
|
||||
}
|
||||
|
||||
try {
|
||||
const params = {
|
||||
key: this.apiKey,
|
||||
address: address.trim(),
|
||||
language: options.language || 'en'
|
||||
};
|
||||
|
||||
// Add component restrictions (e.g., country)
|
||||
if (options.componentRestrictions) {
|
||||
params.components = Object.entries(options.componentRestrictions)
|
||||
.map(([key, value]) => `${key}:${value}`)
|
||||
.join('|');
|
||||
}
|
||||
|
||||
// Add bounds if provided
|
||||
if (options.bounds) {
|
||||
params.bounds = options.bounds;
|
||||
}
|
||||
|
||||
const response = await this.client.geocode({
|
||||
params,
|
||||
timeout: 5000
|
||||
});
|
||||
|
||||
if (response.data.status === 'OK' && response.data.results.length > 0) {
|
||||
const result = response.data.results[0];
|
||||
|
||||
return {
|
||||
latitude: result.geometry.location.lat,
|
||||
longitude: result.geometry.location.lng,
|
||||
formattedAddress: result.formatted_address,
|
||||
placeId: result.place_id
|
||||
};
|
||||
} else {
|
||||
console.error('Geocoding API error:', response.data.status, response.data.error_message);
|
||||
return {
|
||||
error: this.getErrorMessage(response.data.status),
|
||||
status: response.data.status
|
||||
};
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Geocoding service error:', error.message);
|
||||
throw new Error('Failed to geocode address');
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get human-readable error message for Google Maps API status codes
|
||||
*/
|
||||
getErrorMessage(status) {
|
||||
const errorMessages = {
|
||||
'ZERO_RESULTS': 'No results found for this query',
|
||||
'OVER_QUERY_LIMIT': 'API quota exceeded. Please try again later',
|
||||
'REQUEST_DENIED': 'API request denied. Check API key configuration',
|
||||
'INVALID_REQUEST': 'Invalid request parameters',
|
||||
'UNKNOWN_ERROR': 'Server error. Please try again',
|
||||
'NOT_FOUND': 'The specified place was not found'
|
||||
};
|
||||
|
||||
return errorMessages[status] || `Google Maps API error: ${status}`;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Check if the service is properly configured
|
||||
*/
|
||||
isConfigured() {
|
||||
return !!this.apiKey;
|
||||
}
|
||||
}
|
||||
|
||||
// Export singleton instance
|
||||
module.exports = new GoogleMapsService();
|
||||
Reference in New Issue
Block a user