diff --git a/backend/.env.example b/backend/.env.example
index fb6ae87..d4ad23a 100644
--- a/backend/.env.example
+++ b/backend/.env.example
@@ -5,3 +5,5 @@ DB_DATABASE=auth_db
DB_HOST=localhost
JWT_SECRET=«generate_strong_secret_here»
JWT_EXPIRES_IN=604800
+
+GOOGLE_API_KEY=
diff --git a/backend/config/index.js b/backend/config/index.js
index eeb848e..7747667 100644
--- a/backend/config/index.js
+++ b/backend/config/index.js
@@ -11,4 +11,7 @@ module.exports = {
secret: process.env.JWT_SECRET,
expiresIn: process.env.JWT_EXPIRES_IN,
},
+ apiKeys: {
+ google: process.env.GOOGLE_API_KEY,
+ },
};
diff --git a/backend/package-lock.json b/backend/package-lock.json
index 7db4a3f..44b01d0 100644
--- a/backend/package-lock.json
+++ b/backend/package-lock.json
@@ -4,6 +4,26 @@
"lockfileVersion": 1,
"requires": true,
"dependencies": {
+ "@googlemaps/google-maps-services-js": {
+ "version": "3.3.4",
+ "resolved": "https://registry.npmjs.org/@googlemaps/google-maps-services-js/-/google-maps-services-js-3.3.4.tgz",
+ "integrity": "sha512-EhICEyVklA87+lqOjlAwGkkVpwXWKgYX93wGWtWUFHizKcAALKLwx2pDb/0bJy/t/6QCFbOokEuStkwljf2xQA==",
+ "requires": {
+ "@googlemaps/url-signature": "^1.0.4",
+ "agentkeepalive": "^4.1.0",
+ "axios": "^0.24.0",
+ "query-string": "^7.0.1",
+ "retry-axios": "^2.2.1"
+ }
+ },
+ "@googlemaps/url-signature": {
+ "version": "1.0.8",
+ "resolved": "https://registry.npmjs.org/@googlemaps/url-signature/-/url-signature-1.0.8.tgz",
+ "integrity": "sha512-DJL0o1voIx+VntZC9PFZuho3M0PDy44tjpGxFjf2BuOyvd8FKknyN8OGuHf5GQp8rKZzUU0Ng+V8/m5h9wYW/A==",
+ "requires": {
+ "crypto-js": "^4.1.1"
+ }
+ },
"@sindresorhus/is": {
"version": "0.14.0",
"resolved": "https://registry.npmjs.org/@sindresorhus/is/-/is-0.14.0.tgz",
@@ -44,6 +64,31 @@
"negotiator": "0.6.2"
}
},
+ "agentkeepalive": {
+ "version": "4.1.4",
+ "resolved": "https://registry.npmjs.org/agentkeepalive/-/agentkeepalive-4.1.4.tgz",
+ "integrity": "sha512-+V/rGa3EuU74H6wR04plBb7Ks10FbtUQgRj/FQOG7uUIEuaINI+AiqJR1k6t3SVNs7o7ZjIdus6706qqzVq8jQ==",
+ "requires": {
+ "debug": "^4.1.0",
+ "depd": "^1.1.2",
+ "humanize-ms": "^1.2.1"
+ },
+ "dependencies": {
+ "debug": {
+ "version": "4.3.3",
+ "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.3.tgz",
+ "integrity": "sha512-/zxw5+vh1Tfv+4Qn7a5nsbcJKPaSvCDhojn6FEl9vupwK2VCSDtEiEtqr8DFtzYFOdz63LBkxec7DYuc2jon6Q==",
+ "requires": {
+ "ms": "2.1.2"
+ }
+ },
+ "ms": {
+ "version": "2.1.2",
+ "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz",
+ "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w=="
+ }
+ }
+ },
"ansi-align": {
"version": "3.0.1",
"resolved": "https://registry.npmjs.org/ansi-align/-/ansi-align-3.0.1.tgz",
@@ -126,6 +171,14 @@
"resolved": "https://registry.npmjs.org/array-flatten/-/array-flatten-1.1.1.tgz",
"integrity": "sha1-ml9pkFGx5wczKPKgCJaLZOopVdI="
},
+ "axios": {
+ "version": "0.24.0",
+ "resolved": "https://registry.npmjs.org/axios/-/axios-0.24.0.tgz",
+ "integrity": "sha512-Q6cWsys88HoPgAaFAVUb0WpPk0O8iTeisR9IMqy9G8AbO4NlpVknrnQS03zzF9PGAWgO3cgletO3VjV/P7VztA==",
+ "requires": {
+ "follow-redirects": "^1.14.4"
+ }
+ },
"balanced-match": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz",
@@ -576,6 +629,11 @@
"which": "^2.0.1"
}
},
+ "crypto-js": {
+ "version": "4.1.1",
+ "resolved": "https://registry.npmjs.org/crypto-js/-/crypto-js-4.1.1.tgz",
+ "integrity": "sha512-o2JlM7ydqd3Qk9CA0L4NL6mTzU2sdx96a+oOfPu8Mkl/PK51vSyoi8/rQ8NknZtk44vq15lmhAj9CIAGwgeWKw=="
+ },
"crypto-random-string": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/crypto-random-string/-/crypto-random-string-2.0.0.tgz",
@@ -644,6 +702,11 @@
"resolved": "https://registry.npmjs.org/decamelize/-/decamelize-1.2.0.tgz",
"integrity": "sha1-9lNNFRSCabIDUue+4m9QH5oZEpA="
},
+ "decode-uri-component": {
+ "version": "0.2.0",
+ "resolved": "https://registry.npmjs.org/decode-uri-component/-/decode-uri-component-0.2.0.tgz",
+ "integrity": "sha1-6zkTMzRYd1y4TNGh+uBiEGu4dUU="
+ },
"decompress-response": {
"version": "3.3.0",
"resolved": "https://registry.npmjs.org/decompress-response/-/decompress-response-3.3.0.tgz",
@@ -921,6 +984,11 @@
"to-regex-range": "^5.0.1"
}
},
+ "filter-obj": {
+ "version": "1.1.0",
+ "resolved": "https://registry.npmjs.org/filter-obj/-/filter-obj-1.1.0.tgz",
+ "integrity": "sha1-mzERErxsYSehbgFsbF1/GeCAXFs="
+ },
"finalhandler": {
"version": "1.1.2",
"resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-1.1.2.tgz",
@@ -943,6 +1011,11 @@
"locate-path": "^3.0.0"
}
},
+ "follow-redirects": {
+ "version": "1.14.6",
+ "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.14.6.tgz",
+ "integrity": "sha512-fhUl5EwSJbbl8AR+uYL2KQDxLkdSjZGR36xy46AO7cOMTrCMON6Sa28FmAnC2tRTDbd/Uuzz3aJBv7EBN7JH8A=="
+ },
"forwarded": {
"version": "0.2.0",
"resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.2.0.tgz",
@@ -1112,6 +1185,14 @@
}
}
},
+ "humanize-ms": {
+ "version": "1.2.1",
+ "resolved": "https://registry.npmjs.org/humanize-ms/-/humanize-ms-1.2.1.tgz",
+ "integrity": "sha1-xG4xWaKT9riW2ikxbYtv6Lt5u+0=",
+ "requires": {
+ "ms": "^2.0.0"
+ }
+ },
"iconv-lite": {
"version": "0.4.24",
"resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.24.tgz",
@@ -1879,6 +1960,17 @@
"resolved": "https://registry.npmjs.org/qs/-/qs-6.7.0.tgz",
"integrity": "sha512-VCdBRNFTX1fyE7Nb6FYoURo/SPe62QCaAyzJvUjwRaIsc+NePBEniHlvxFmmX56+HZphIGtV0XeCirBtpDrTyQ=="
},
+ "query-string": {
+ "version": "7.0.1",
+ "resolved": "https://registry.npmjs.org/query-string/-/query-string-7.0.1.tgz",
+ "integrity": "sha512-uIw3iRvHnk9to1blJCG3BTc+Ro56CBowJXKmNNAm3RulvPBzWLRqKSiiDk+IplJhsydwtuNMHi8UGQFcCLVfkA==",
+ "requires": {
+ "decode-uri-component": "^0.2.0",
+ "filter-obj": "^1.1.0",
+ "split-on-first": "^1.0.0",
+ "strict-uri-encode": "^2.0.0"
+ }
+ },
"random-bytes": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/random-bytes/-/random-bytes-1.0.0.tgz",
@@ -2004,6 +2096,11 @@
"any-promise": "^1.3.0"
}
},
+ "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=="
+ },
"rndm": {
"version": "1.2.0",
"resolved": "https://registry.npmjs.org/rndm/-/rndm-1.2.0.tgz",
@@ -2199,6 +2296,11 @@
"integrity": "sha512-sDl4qMFpijcGw22U5w63KmD3cZJfBuFlVNbVMKje2keoKML7X2UzWbc4XrmEbDwg0NXJc3yv4/ox7b+JWb57kQ==",
"dev": true
},
+ "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=="
+ },
"split2": {
"version": "3.2.2",
"resolved": "https://registry.npmjs.org/split2/-/split2-3.2.2.tgz",
@@ -2217,6 +2319,11 @@
"resolved": "https://registry.npmjs.org/statuses/-/statuses-1.5.0.tgz",
"integrity": "sha1-Fhx9rBd2Wf2YEfQ3cfqZOBR4Yow="
},
+ "strict-uri-encode": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/strict-uri-encode/-/strict-uri-encode-2.0.0.tgz",
+ "integrity": "sha1-ucczDHBChi9rFC3CdLvMWGbONUY="
+ },
"string-width": {
"version": "3.1.0",
"resolved": "https://registry.npmjs.org/string-width/-/string-width-3.1.0.tgz",
diff --git a/backend/package.json b/backend/package.json
index ed7123f..abb8b5b 100644
--- a/backend/package.json
+++ b/backend/package.json
@@ -14,6 +14,7 @@
"author": "",
"license": "ISC",
"dependencies": {
+ "@googlemaps/google-maps-services-js": "^3.3.4",
"bcryptjs": "^2.4.3",
"cookie-parser": "^1.4.6",
"cors": "^2.8.5",
diff --git a/backend/routes/api/business.js b/backend/routes/api/business.js
index 09832fe..77692da 100644
--- a/backend/routes/api/business.js
+++ b/backend/routes/api/business.js
@@ -1,10 +1,14 @@
const asyncHandler = require("express-async-handler");
const createHttpError = require("http-errors");
const express = require("express");
-const { check, query } = require("express-validator");
+const { Client } = require("@googlemaps/google-maps-services-js");
+const { check, query, validationResult } = require("express-validator");
const { Op } = require("sequelize");
const { Business, Review, User } = require("../../db/models");
+const {
+ apiKeys: { google: googleApiKey },
+} = require("../../config");
const {
handleValidationErrors,
sanitizePaginationQuery,
@@ -83,9 +87,44 @@ router.get(
})
);
+const client = new Client();
+const addressToLatLong = asyncHandler(async (req) => {
+ // If the lat and long are passed in the body, skip requesting the information from Google
+ if (req.body.lat && req.body.long) {
+ return { lat: req.body.lat, long: req.body.long };
+ }
+
+ const validationErrors = validationResult(req);
+
+ if (!validationErrors.isEmpty()) {
+ return;
+ }
+
+ const { address, city, state, zipCode } = req.body;
+
+ const geocodeResult = await client.geocode({
+ params: {
+ address: `${address}, ${city}, ${state} ${zipCode}`,
+ key: googleApiKey,
+ },
+ });
+
+ if (
+ !geocodeResult.data ||
+ geocodeResult.data.status !== "OK" ||
+ !geocodeResult.data.results ||
+ geocodeResult.data.results.length < 1
+ ) {
+ return;
+ }
+
+ return {
+ lat: geocodeResult.data.results[0].geometry.location.lat,
+ long: geocodeResult.data.results[0].geometry.location.lng,
+ };
+});
+
const validateBusiness = [
- check("name").trim().exists({ checkFalsy: true }).withMessage("Enter a name"),
- check("description").trim().optional({ checkFalsy: true }),
check("address")
.trim()
.exists({ checkFalsy: true })
@@ -99,6 +138,29 @@ const validateBusiness = [
.trim()
.exists({ checkFalsy: true })
.withMessage("Enter a zip code"),
+ check("address").custom(async (_, { req }) => {
+ try {
+ const latLong = await addressToLatLong(req);
+
+ if (!latLong) {
+ throw new Error("Failed to find this address. Try again.");
+ }
+
+ req.body.lat = latLong.lat;
+ req.body.long = latLong.long;
+ return true;
+ } catch {
+ throw new Error("Failed to find this address. Try again.");
+ }
+ }),
+
+ check("name").trim().exists({ checkFalsy: true }).withMessage("Enter a name"),
+ check("description").trim().optional({ checkFalsy: true }),
+ check("displayImage")
+ .trim()
+ .optional({ checkFalsy: true })
+ .isURL()
+ .withMessage("Enter a valid image url"),
check("lat")
.exists({ checkFalsy: true })
.withMessage("Enter a latitude")
@@ -109,11 +171,6 @@ const validateBusiness = [
.withMessage("Enter a longitude")
.isDecimal()
.withMessage("Enter a valid longitude"),
- check("displayImage")
- .trim()
- .optional({ checkFalsy: true })
- .isURL()
- .withMessage("Enter a valid image url"),
handleValidationErrors,
];
diff --git a/frontend/src/components/business/BusinessEditor.js b/frontend/src/components/business/BusinessEditor.js
index 6480095..84ad55a 100644
--- a/frontend/src/components/business/BusinessEditor.js
+++ b/frontend/src/components/business/BusinessEditor.js
@@ -45,8 +45,6 @@ const BusinessEditor = ({ addNew }) => {
const [city, setCity] = useState(businessData.city || "");
const [state, setState] = useState(businessData.state || "");
const [zipCode, setZipCode] = useState(businessData.zipCode || "");
- const [lat, setLat] = useState(businessData.lat || "");
- const [long, setLong] = useState(businessData.long || "");
const [displayImage, setDisplayImage] = useState(
businessData.displayImage || ""
);
@@ -73,8 +71,6 @@ const BusinessEditor = ({ addNew }) => {
city,
state,
zipCode,
- lat,
- long,
displayImage,
};
@@ -217,38 +213,6 @@ const BusinessEditor = ({ addNew }) => {
required
/>
-
- setLat(e.target.value)}
- inputProps={{
- type: "number",
- step: "any",
- }}
- error={!!errors.lat}
- helperText={errors.lat}
- required
- />
-
-
- setLong(e.target.value)}
- inputProps={{
- type: "number",
- step: "any",
- }}
- error={!!errors.long}
- helperText={errors.long}
- required
- />
-