From 3d4903682ae8b18a420ca99c02cf6e5bb9990369 Mon Sep 17 00:00:00 2001 From: Aadit Kamat Date: Wed, 6 Mar 2024 19:38:25 -0500 Subject: [PATCH] Send verification email using Mailgun (#195) * Add mailgun dependency * Add REST API to send verification email through Mailgun --- server/package-lock.json | 429 +++++++++++++++++++------------------ server/package.json | 1 + server/src/index.ts | 443 ++++++++++++++++++++++----------------- 3 files changed, 463 insertions(+), 410 deletions(-) diff --git a/server/package-lock.json b/server/package-lock.json index 564e301cd..a085f31ee 100644 --- a/server/package-lock.json +++ b/server/package-lock.json @@ -15,6 +15,7 @@ "express": "^4.18.2", "firebase-admin": "^12.0.0", "geofire-common": "^6.0.0", + "mailgun.js": "^3.7.2", "socket.io": "^4.7.4", "uuid": "^9.0.1" }, @@ -1966,97 +1967,6 @@ "node": ">=10.0.0" } }, - "node_modules/@firebase/webchannel-wrapper": { - "version": "0.10.5", - "resolved": "https://registry.npmjs.org/@firebase/webchannel-wrapper/-/webchannel-wrapper-0.10.5.tgz", - "integrity": "sha512-eSkJsnhBWv5kCTSU1tSUVl9mpFu+5NXXunZc83le8GMjMlsWwQArSc7cJJ4yl+aDFY0NGLi0AjZWMn1axOrkRg==" - }, - "node_modules/@google-cloud/firestore": { - "version": "7.3.0", - "resolved": "https://registry.npmjs.org/@google-cloud/firestore/-/firestore-7.3.0.tgz", - "integrity": "sha512-2IftQLAbCuVp0nTd3neeu+d3OYIegJpV/V9R4USQj51LzJcXPe8h8jZ7j3+svSNhJVGy6JsN0T1QqlJdMDhTwg==", - "optional": true, - "dependencies": { - "fast-deep-equal": "^3.1.1", - "functional-red-black-tree": "^1.0.1", - "google-gax": "^4.0.4", - "protobufjs": "^7.2.5" - }, - "engines": { - "node": ">=14.0.0" - } - }, - "node_modules/@google-cloud/paginator": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/@google-cloud/paginator/-/paginator-5.0.0.tgz", - "integrity": "sha512-87aeg6QQcEPxGCOthnpUjvw4xAZ57G7pL8FS0C4e/81fr3FjkpUpibf1s2v5XGyGhUVGF4Jfg7yEcxqn2iUw1w==", - "optional": true, - "dependencies": { - "arrify": "^2.0.0", - "extend": "^3.0.2" - }, - "engines": { - "node": ">=14.0.0" - } - }, - "node_modules/@google-cloud/projectify": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/@google-cloud/projectify/-/projectify-4.0.0.tgz", - "integrity": "sha512-MmaX6HeSvyPbWGwFq7mXdo0uQZLGBYCwziiLIGq5JVX+/bdI3SAq6bP98trV5eTWfLuvsMcIC1YJOF2vfteLFA==", - "optional": true, - "engines": { - "node": ">=14.0.0" - } - }, - "node_modules/@google-cloud/promisify": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/@google-cloud/promisify/-/promisify-4.0.0.tgz", - "integrity": "sha512-Orxzlfb9c67A15cq2JQEyVc7wEsmFBmHjZWZYQMUyJ1qivXyMwdyNOs9odi79hze+2zqdTtu1E19IM/FtqZ10g==", - "optional": true, - "engines": { - "node": ">=14" - } - }, - "node_modules/@google-cloud/storage": { - "version": "7.7.0", - "resolved": "https://registry.npmjs.org/@google-cloud/storage/-/storage-7.7.0.tgz", - "integrity": "sha512-EMCEY+6JiIkx7Dt8NXVGGjy1vRdSGdHkoqZoqjJw7cEBkT7ZkX0c7puedfn1MamnzW5SX4xoa2jVq5u7OWBmkQ==", - "optional": true, - "dependencies": { - "@google-cloud/paginator": "^5.0.0", - "@google-cloud/projectify": "^4.0.0", - "@google-cloud/promisify": "^4.0.0", - "abort-controller": "^3.0.0", - "async-retry": "^1.3.3", - "compressible": "^2.0.12", - "duplexify": "^4.0.0", - "ent": "^2.2.0", - "fast-xml-parser": "^4.3.0", - "gaxios": "^6.0.2", - "google-auth-library": "^9.0.0", - "mime": "^3.0.0", - "mime-types": "^2.0.8", - "p-limit": "^3.0.1", - "retry-request": "^7.0.0", - "teeny-request": "^9.0.0", - "uuid": "^8.0.0" - }, - "engines": { - "node": ">=14" - } - }, - "node_modules/@google-cloud/storage/node_modules/mime": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/mime/-/mime-3.0.0.tgz", - "integrity": "sha512-jSCU7/VB1loIWBZe14aEYHU/+1UMEHoaO7qxCOVJOw9GgH72VAWppxNcjU+x9a2k3GSIBXNKxXQFqRvvZ7vr3A==", - "optional": true, - "bin": { - "mime": "cli.js" - }, - "engines": { - "node": ">=10.0.0" - } - }, "node_modules/@google-cloud/storage/node_modules/uuid": { "version": "8.3.2", "resolved": "https://registry.npmjs.org/uuid/-/uuid-8.3.2.tgz", @@ -2066,18 +1976,6 @@ "uuid": "dist/bin/uuid" } }, - "node_modules/@grpc/grpc-js": { - "version": "1.9.14", - "resolved": "https://registry.npmjs.org/@grpc/grpc-js/-/grpc-js-1.9.14.tgz", - "integrity": "sha512-nOpuzZ2G3IuMFN+UPPpKrC6NsLmWsTqSsm66IRfnBt1D4pwTqE27lmbpcPM+l2Ua4gE7PfjRHI6uedAy7hoXUw==", - "dependencies": { - "@grpc/proto-loader": "^0.7.8", - "@types/node": ">=12.12.47" - }, - "engines": { - "node": "^8.13.0 || >=10.10.0" - } - }, "node_modules/@grpc/proto-loader": { "version": "0.7.10", "resolved": "https://registry.npmjs.org/@grpc/proto-loader/-/proto-loader-0.7.10.tgz", @@ -3189,7 +3087,6 @@ "version": "3.0.0", "resolved": "https://registry.npmjs.org/abort-controller/-/abort-controller-3.0.0.tgz", "integrity": "sha512-h8lQ8tacZYnR3vNQTgibj+tODHI5/+l06Au2Pcriv/Gmet0eaj4TwWH41sO9wnHDiQsEj19q0drzdWdeAHtweg==", - "optional": true, "dependencies": { "event-target-shim": "^5.0.0" }, @@ -3566,6 +3463,11 @@ "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", "dev": true }, + "node_modules/base-64": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/base-64/-/base-64-1.0.0.tgz", + "integrity": "sha512-kwDPIFCGx0NZHog36dj+tHiwP4QMzsZ3AgMViUBKI0+V5n4U0ufTCUMhnQ04diaRI8EX/QcPfql7zlhZ7j4zgg==" + }, "node_modules/base64-js": { "version": "1.5.1", "resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.5.1.tgz", @@ -3612,6 +3514,11 @@ "node": ">=8" } }, + "node_modules/bluebird": { + "version": "3.7.2", + "resolved": "https://registry.npmjs.org/bluebird/-/bluebird-3.7.2.tgz", + "integrity": "sha512-XpNj6GDQzdfW+r2Wnn7xiSAd7TM3jzkxGXBGTtWKuSXv1xUV+azxAm8jdWZN06QTQk+2N2XB9jRDkvbmQmcRtg==" + }, "node_modules/body-parser": { "version": "1.20.1", "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.20.1.tgz", @@ -3868,6 +3775,19 @@ "node": ">=12" } }, + "node_modules/clone-deep": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/clone-deep/-/clone-deep-4.0.1.tgz", + "integrity": "sha512-neHB9xuzh/wk0dIHweyAXv2aPGZIVk3pLMe+/RNzINf17fe0OG96QroktYAUm7SM1PBnzTabaLboqqxDyMU+SQ==", + "dependencies": { + "is-plain-object": "^2.0.4", + "kind-of": "^6.0.2", + "shallow-clone": "^3.0.0" + }, + "engines": { + "node": ">=6" + } + }, "node_modules/co": { "version": "4.6.0", "resolved": "https://registry.npmjs.org/co/-/co-4.6.0.tgz", @@ -4102,6 +4022,14 @@ "node": ">= 8" } }, + "node_modules/data-uri-to-buffer": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/data-uri-to-buffer/-/data-uri-to-buffer-3.0.1.tgz", + "integrity": "sha512-WboRycPNsVw3B3TL559F7kuBUM4d8CgMEvk6xEJlOp7OBPjt6G7z8WMWlD2rOFZLk6OYfFIUGsCOWzcQH9K2og==", + "engines": { + "node": ">= 6" + } + }, "node_modules/debug": { "version": "4.3.4", "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz", @@ -4426,7 +4354,6 @@ "version": "5.0.1", "resolved": "https://registry.npmjs.org/event-target-shim/-/event-target-shim-5.0.1.tgz", "integrity": "sha512-i/2XbnSz/uxRCU6+NdVJgKWDTM427+MqYbkQzD321DuCQJUqOuJKIA0IM2+W2xtYHdKOmZ4dR6fExsd4SXL+WQ==", - "optional": true, "engines": { "node": ">=6" } @@ -4593,6 +4520,19 @@ "bser": "2.1.1" } }, + "node_modules/fetch-blob": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/fetch-blob/-/fetch-blob-2.1.2.tgz", + "integrity": "sha512-YKqtUDwqLyfyMnmbw8XD6Q8j9i/HggKtPEI+pZ1+8bvheBu78biSmNaXWusx1TauGqtUUGx/cBb1mKdq2rLYow==", + "engines": { + "node": "^10.17.0 || >=12.3.0" + }, + "peerDependenciesMeta": { + "domexception": { + "optional": true + } + } + }, "node_modules/fill-range": { "version": "7.0.1", "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.0.1.tgz", @@ -4681,37 +4621,12 @@ "node": ">=14" } }, - "node_modules/firebase-admin": { - "version": "12.0.0", - "resolved": "https://registry.npmjs.org/firebase-admin/-/firebase-admin-12.0.0.tgz", - "integrity": "sha512-wBrrSSsKV++/+O8E7O/C7/wL0nbG/x4Xv4yatz/+sohaZ+LsnWtYUcrd3gZutO86hLpDex7xgyrkKbgulmtVyQ==", - "dependencies": { - "@fastify/busboy": "^1.2.1", - "@firebase/database-compat": "^1.0.2", - "@firebase/database-types": "^1.0.0", - "@types/node": "^20.10.3", - "jsonwebtoken": "^9.0.0", - "jwks-rsa": "^3.0.1", - "node-forge": "^1.3.1", - "uuid": "^9.0.0" - }, - "engines": { - "node": ">=14" - }, - "optionalDependencies": { - "@google-cloud/firestore": "^7.1.0", - "@google-cloud/storage": "^7.7.0" - } - }, - "node_modules/firebase-admin/node_modules/@fastify/busboy": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/@fastify/busboy/-/busboy-1.2.1.tgz", - "integrity": "sha512-7PQA7EH43S0CxcOa9OeAnaeA0oQ+e/DHNPZwSQM9CQHW76jle5+OvLdibRp/Aafs9KXbLhxyjOTkRjWUbQEd3Q==", - "dependencies": { - "text-decoding": "^1.0.0" - }, - "engines": { - "node": ">=14" + "node_modules/flat": { + "version": "5.0.2", + "resolved": "https://registry.npmjs.org/flat/-/flat-5.0.2.tgz", + "integrity": "sha512-b6suED+5/3rTpUBdG1gupIl8MPFCAMA0QXwmljLhvCUKcUvdE4gWky9zpuGCcXHOsz4J9wPGNWq6OKpmIzz3hQ==", + "bin": { + "flat": "cli.js" } }, "node_modules/follow-redirects": { @@ -5001,80 +4916,6 @@ "node": "^8.13.0 || >=10.10.0" } }, - "node_modules/google-auth-library": { - "version": "9.6.3", - "resolved": "https://registry.npmjs.org/google-auth-library/-/google-auth-library-9.6.3.tgz", - "integrity": "sha512-4CacM29MLC2eT9Cey5GDVK4Q8t+MMp8+OEdOaqD9MG6b0dOyLORaaeJMPQ7EESVgm/+z5EKYyFLxgzBJlJgyHQ==", - "optional": true, - "dependencies": { - "base64-js": "^1.3.0", - "ecdsa-sig-formatter": "^1.0.11", - "gaxios": "^6.1.1", - "gcp-metadata": "^6.1.0", - "gtoken": "^7.0.0", - "jws": "^4.0.0" - }, - "engines": { - "node": ">=14" - } - }, - "node_modules/google-auth-library/node_modules/jwa": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/jwa/-/jwa-2.0.0.tgz", - "integrity": "sha512-jrZ2Qx916EA+fq9cEAeCROWPTfCwi1IVHqT2tapuqLEVVDKFDENFw1oL+MwrTvH6msKxsd1YTDVw6uKEcsrLEA==", - "optional": true, - "dependencies": { - "buffer-equal-constant-time": "1.0.1", - "ecdsa-sig-formatter": "1.0.11", - "safe-buffer": "^5.0.1" - } - }, - "node_modules/google-auth-library/node_modules/jws": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/jws/-/jws-4.0.0.tgz", - "integrity": "sha512-KDncfTmOZoOMTFG4mBlG0qUIOlc03fmzH+ru6RgYVZhPkyiy/92Owlt/8UEN+a4TXR1FQetfIpJE8ApdvdVxTg==", - "optional": true, - "dependencies": { - "jwa": "^2.0.0", - "safe-buffer": "^5.0.1" - } - }, - "node_modules/google-gax": { - "version": "4.3.1", - "resolved": "https://registry.npmjs.org/google-gax/-/google-gax-4.3.1.tgz", - "integrity": "sha512-qpSfslpwqToIgQ+Tf3MjWIDjYK4UFIZ0uz6nLtttlW9N1NQA4PhGf9tlGo6KDYJ4rgL2w4CjXVd0z5yeNpN/Iw==", - "optional": true, - "dependencies": { - "@grpc/grpc-js": "~1.10.0", - "@grpc/proto-loader": "^0.7.0", - "@types/long": "^4.0.0", - "abort-controller": "^3.0.0", - "duplexify": "^4.0.0", - "google-auth-library": "^9.3.0", - "node-fetch": "^2.6.1", - "object-hash": "^3.0.0", - "proto3-json-serializer": "^2.0.0", - "protobufjs": "7.2.6", - "retry-request": "^7.0.0", - "uuid": "^9.0.1" - }, - "engines": { - "node": ">=14" - } - }, - "node_modules/google-gax/node_modules/@grpc/grpc-js": { - "version": "1.10.1", - "resolved": "https://registry.npmjs.org/@grpc/grpc-js/-/grpc-js-1.10.1.tgz", - "integrity": "sha512-55ONqFytZExfOIjF1RjXPcVmT/jJqFzbbDqxK9jmRV4nxiYWtL9hENSW1Jfx0SdZfrvoqd44YJ/GJTqfRrawSQ==", - "optional": true, - "dependencies": { - "@grpc/proto-loader": "^0.7.8", - "@types/node": ">=12.12.47" - }, - "engines": { - "node": "^8.13.0 || >=10.10.0" - } - }, "node_modules/gopd": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.0.1.tgz", @@ -5399,6 +5240,17 @@ "node": ">=0.12.0" } }, + "node_modules/is-plain-object": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/is-plain-object/-/is-plain-object-2.0.4.tgz", + "integrity": "sha512-h5PpgXkWitc38BBMYawTYMWJHFZJVnBquFE57xFpjB8pJFiF6gZ+bU+WyI/yqXiFR5mdLsgYNaPe8uao6Uv9Og==", + "dependencies": { + "isobject": "^3.0.1" + }, + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/is-stream": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-2.0.1.tgz", @@ -5417,6 +5269,14 @@ "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==", "dev": true }, + "node_modules/isobject": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/isobject/-/isobject-3.0.1.tgz", + "integrity": "sha512-WhB9zCku7EGTj/HQQRz5aUQEUeoQZH2bWcltRErOpymJ4boYE6wL9Tbr23krRPSZ+C5zqNSrSw+Cc7sZZ4b7vg==", + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/istanbul-lib-coverage": { "version": "3.2.2", "resolved": "https://registry.npmjs.org/istanbul-lib-coverage/-/istanbul-lib-coverage-3.2.2.tgz", @@ -7266,6 +7126,14 @@ "safe-buffer": "^5.0.1" } }, + "node_modules/kind-of": { + "version": "6.0.3", + "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-6.0.3.tgz", + "integrity": "sha512-dcS1ul+9tmeD95T+x28/ehLgd9mENa3LsvDTtzm3vyBEO7RPptvAD+t44WVXaUjTBRcrpFeFlC8WCruUR456hw==", + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/kleur": { "version": "3.0.3", "resolved": "https://registry.npmjs.org/kleur/-/kleur-3.0.3.tgz", @@ -7275,6 +7143,57 @@ "node": ">=6" } }, + "node_modules/ky": { + "version": "0.25.1", + "resolved": "https://registry.npmjs.org/ky/-/ky-0.25.1.tgz", + "integrity": "sha512-PjpCEWlIU7VpiMVrTwssahkYXX1by6NCT0fhTUX34F3DTinARlgMpriuroolugFPcMgpPWrOW4mTb984Qm1RXA==", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sindresorhus/ky?sponsor=1" + } + }, + "node_modules/ky-universal": { + "version": "0.8.2", + "resolved": "https://registry.npmjs.org/ky-universal/-/ky-universal-0.8.2.tgz", + "integrity": "sha512-xe0JaOH9QeYxdyGLnzUOVGK4Z6FGvDVzcXFTdrYA1f33MZdEa45sUDaMBy98xQMcsd2XIBrTXRrRYnegcSdgVQ==", + "dependencies": { + "abort-controller": "^3.0.0", + "node-fetch": "3.0.0-beta.9" + }, + "engines": { + "node": ">=10.17" + }, + "funding": { + "url": "https://github.com/sindresorhus/ky-universal?sponsor=1" + }, + "peerDependencies": { + "ky": ">=0.17.0", + "web-streams-polyfill": ">=2.0.0" + }, + "peerDependenciesMeta": { + "web-streams-polyfill": { + "optional": true + } + } + }, + "node_modules/ky-universal/node_modules/node-fetch": { + "version": "3.0.0-beta.9", + "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-3.0.0-beta.9.tgz", + "integrity": "sha512-RdbZCEynH2tH46+tj0ua9caUHVWrd/RHnRfvly2EVdqGmI3ndS1Vn/xjm5KuGejDt2RNDQsVRLPNd2QPwcewVg==", + "dependencies": { + "data-uri-to-buffer": "^3.0.1", + "fetch-blob": "^2.1.1" + }, + "engines": { + "node": "^10.17 || >=12.3" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/node-fetch" + } + }, "node_modules/leven": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/leven/-/leven-3.1.0.tgz", @@ -7318,11 +7237,6 @@ "resolved": "https://registry.npmjs.org/lodash.clonedeep/-/lodash.clonedeep-4.5.0.tgz", "integrity": "sha512-H5ZhCF25riFd9uB5UCkVKo61m3S/xZk1x4wA6yp/L3RFP6Z/eHH1ymQcGLo7J3GMPfm0V/7m1tryHuGVxpqEBQ==" }, - "node_modules/lodash.clonedeep": { - "version": "4.5.0", - "resolved": "https://registry.npmjs.org/lodash.clonedeep/-/lodash.clonedeep-4.5.0.tgz", - "integrity": "sha512-H5ZhCF25riFd9uB5UCkVKo61m3S/xZk1x4wA6yp/L3RFP6Z/eHH1ymQcGLo7J3GMPfm0V/7m1tryHuGVxpqEBQ==" - }, "node_modules/lodash.debounce": { "version": "4.0.8", "resolved": "https://registry.npmjs.org/lodash.debounce/-/lodash.debounce-4.0.8.tgz", @@ -7402,6 +7316,21 @@ "resolved": "https://registry.npmjs.org/yallist/-/yallist-2.1.2.tgz", "integrity": "sha512-ncTzHV7NvsQZkYe1DW7cbDLm0YpzHmZF5r/iyP3ZnQtMiJ+pjzisCiMNI+Sj+xQF5pXhSHxSB3uDbsBTzY/c2A==" }, + "node_modules/mailgun.js": { + "version": "3.7.2", + "resolved": "https://registry.npmjs.org/mailgun.js/-/mailgun.js-3.7.2.tgz", + "integrity": "sha512-vPU6iz32/j7vZ8LN+hN5cim8lBke71Qm83rbPN2Gh+JPJ6ClqdxUMKFrAW36mOodnHVZKpTWrSATNXGxT3cHVQ==", + "dependencies": { + "base-64": "^1.0.0", + "bluebird": "^3.7.2", + "ky": "^0.25.1", + "ky-universal": "^0.8.2", + "url": "^0.11.0", + "url-join": "0.0.1", + "web-streams-polyfill": "^3.0.1", + "webpack-merge": "^5.4.0" + } + }, "node_modules/make-dir": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/make-dir/-/make-dir-4.0.0.tgz", @@ -8029,6 +7958,11 @@ "integrity": "sha512-77DZwxQmxKnu3aR542U+X8FypNzbfJ+C5XQDk3uWjWxn6151aIMGthWYRXTqT1E5oJvg+ljaa2OJi+VfvCOQ8w==", "dev": true }, + "node_modules/punycode": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/punycode/-/punycode-1.4.1.tgz", + "integrity": "sha512-jmYNElW7yvO7TV33CjSmvSiE2yco3bV2czu/OzDKdMNVZQWfxCblURLhf+47syQRBntjfLdd/H0egrzIG+oaFQ==" + }, "node_modules/pure-rand": { "version": "6.0.4", "resolved": "https://registry.npmjs.org/pure-rand/-/pure-rand-6.0.4.tgz", @@ -8372,6 +8306,17 @@ "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.2.0.tgz", "integrity": "sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==" }, + "node_modules/shallow-clone": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/shallow-clone/-/shallow-clone-3.0.1.tgz", + "integrity": "sha512-/6KqX+GVUdqPuPPd2LxDDxzX6CAbjJehAAOKlNpqqUpAqPM6HeL8f+o3a+JsyGjn2lv0WY8UsTgUJjU9Ok55NA==", + "dependencies": { + "kind-of": "^6.0.2" + }, + "engines": { + "node": ">=8" + } + }, "node_modules/shebang-command": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", @@ -9007,6 +8952,34 @@ "browserslist": ">= 4.21.0" } }, + "node_modules/url": { + "version": "0.11.3", + "resolved": "https://registry.npmjs.org/url/-/url-0.11.3.tgz", + "integrity": "sha512-6hxOLGfZASQK/cijlZnZJTq8OXAkt/3YGfQX45vvMYXpZoo8NdWZcY73K108Jf759lS1Bv/8wXnHDTSz17dSRw==", + "dependencies": { + "punycode": "^1.4.1", + "qs": "^6.11.2" + } + }, + "node_modules/url-join": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/url-join/-/url-join-0.0.1.tgz", + "integrity": "sha512-H6dnQ/yPAAVzMQRvEvyz01hhfQL5qRWSEt7BX8t9DqnPw9BjMb64fjIRq76Uvf1hkHp+mTZvEVJ5guXOT0Xqaw==" + }, + "node_modules/url/node_modules/qs": { + "version": "6.11.2", + "resolved": "https://registry.npmjs.org/qs/-/qs-6.11.2.tgz", + "integrity": "sha512-tDNIz22aBzCDxLtVH++VnTfzxlfeK5CbqohpSqpJgj1Wg/cQbStNAz3NuqCs5vV+pjBsK4x4pN9HlVh7rcYRiA==", + "dependencies": { + "side-channel": "^1.0.4" + }, + "engines": { + "node": ">=0.6" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/util-deprecate": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", @@ -9070,12 +9043,33 @@ "makeerror": "1.0.12" } }, + "node_modules/web-streams-polyfill": { + "version": "3.3.3", + "resolved": "https://registry.npmjs.org/web-streams-polyfill/-/web-streams-polyfill-3.3.3.tgz", + "integrity": "sha512-d2JWLCivmZYTSIoge9MsgFCZrt571BikcWGYkjC1khllbTeDlGqZ2D8vD8E/lJa8WGWbb7Plm8/XJYV7IJHZZw==", + "engines": { + "node": ">= 8" + } + }, "node_modules/webidl-conversions": { "version": "3.0.1", "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-3.0.1.tgz", "integrity": "sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ==", "optional": true }, + "node_modules/webpack-merge": { + "version": "5.10.0", + "resolved": "https://registry.npmjs.org/webpack-merge/-/webpack-merge-5.10.0.tgz", + "integrity": "sha512-+4zXKdx7UnO+1jaN4l2lHVD+mFvnlZQP/6ljaJVb4SZiwIKeUnrT5l0gkT8z+n4hKpC+jpOv6O9R+gLtag7pSA==", + "dependencies": { + "clone-deep": "^4.0.1", + "flat": "^5.0.2", + "wildcard": "^2.0.0" + }, + "engines": { + "node": ">=10.0.0" + } + }, "node_modules/websocket-driver": { "version": "0.7.4", "resolved": "https://registry.npmjs.org/websocket-driver/-/websocket-driver-0.7.4.tgz", @@ -9122,6 +9116,11 @@ "node": ">= 8" } }, + "node_modules/wildcard": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/wildcard/-/wildcard-2.0.1.tgz", + "integrity": "sha512-CC1bOL87PIWSBhDcTrdeLo6eGT7mCFtrg0uIJtqJUFyK+eJnzl8A1niH56uu7KMa5XFrtiV+AQuHO3n7DsHnLQ==" + }, "node_modules/wrap-ansi": { "version": "7.0.0", "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", diff --git a/server/package.json b/server/package.json index 9af60ca12..093c72eb6 100644 --- a/server/package.json +++ b/server/package.json @@ -30,6 +30,7 @@ "express": "^4.18.2", "firebase-admin": "^12.0.0", "geofire-common": "^6.0.0", + "mailgun.js": "^3.7.2", "socket.io": "^4.7.4", "uuid": "^9.0.1" }, diff --git a/server/src/index.ts b/server/src/index.ts index c2e4932b2..bb247896e 100644 --- a/server/src/index.ts +++ b/server/src/index.ts @@ -1,270 +1,324 @@ -import express from 'express' -import 'dotenv/config' -import 'geofire-common' -import { Message } from './types/Message'; -import { createMessage } from './actions/createMessage' -import { createUser } from './actions/createConnectedUser' -import { toggleUserConnectionStatus, updateUserLocation } from './actions/updateConnectedUser' -import { deleteConnectedUserByUID } from './actions/deleteConnectedUser' -import {geohashForLocation} from 'geofire-common'; -import { findNearbyUsers } from './actions/getConnectedUsers' -import { ConnectedUser } from './types/User'; -import { getAuth } from 'firebase-admin/auth'; - - -const { createServer } = require('http') -const { Server } = require('socket.io') -const socket_port = process.env.socket_port -const express_port = process.env.express_port -const app = express() +import express from "express"; +import "dotenv/config"; +import "geofire-common"; +import { Message } from "./types/Message"; +import { createMessage } from "./actions/createMessage"; +import { createUser } from "./actions/createConnectedUser"; +import { + toggleUserConnectionStatus, + updateUserLocation, +} from "./actions/updateConnectedUser"; +import { deleteConnectedUserByUID } from "./actions/deleteConnectedUser"; +import { geohashForLocation } from "geofire-common"; +import { findNearbyUsers } from "./actions/getConnectedUsers"; +import { ConnectedUser } from "./types/User"; +import { getAuth } from "firebase-admin/auth"; +import Mailgun from "mailgun.js"; + +const { createServer } = require("http"); +const { Server } = require("socket.io"); +const socket_port = process.env.socket_port; +const express_port = process.env.express_port; +const app = express(); // Middleware -app.use(express.json()) -app.use(express.urlencoded({ extended: true })) - - +app.use(express.json()); +app.use(express.urlencoded({ extended: true })); // === SOCKET API === -const socketServer = createServer() +const socketServer = createServer(); const io = new Server(socketServer, { cors: { - origin: '*', - methods: ['GET', 'POST'], + origin: "*", + methods: ["GET", "POST"], }, }); // Firebase JWT Authorization Custom Middleware io.use(async (socket, next) => { const token = socket.handshake.auth.token; - console.log(`[WS] Recieved token: ${token}`) + console.log(`[WS] Recieved token: ${token}`); if (token) { const decodedToken = await getAuth().verifyIdToken(token); const userId = decodedToken.uid; console.log(`[WS] User <${userId}> authenticated.`); - console.log(decodedToken) + console.log(decodedToken); next(); } else { - console.error("[WS] User not authenticated.") - next(new Error('User not authenticated.')); + console.error("[WS] User not authenticated."); + next(new Error("User not authenticated.")); } -}) - +}); -io.on('connection', async (socket: any) => { +io.on("connection", async (socket: any) => { console.log(`[WS] User <${socket.id}> connected.`); const defaultConnectedUser: ConnectedUser = { - uid: "UID", - socketId: socket.id, - displayName: "DISPLAY NAME", - userIcon: { - foregroundImage: "FOREGROUND IMG", - backgroundImage: "BACKGROUND IMG" - }, - location: { - lat: 9999, - lon: 9999, - geohash: "F" - } - } // TODO: Send this info from client on connection - await createUser(defaultConnectedUser) - await toggleUserConnectionStatus(socket.id) - - socket.on('disconnect', () => { - console.log(`[WS] User <${socket.id}> exited.`); - deleteConnectedUserByUID(socket.id) - }) - socket.on('ping', (ack) => { - // The (ack) parameter stands for "acknowledgement." This function sends a message back to the originating socket. - console.log(`[WS] Recieved ping from user <${socket.id}>.`) - if (ack) ack('pong') - }) - socket.on('message', async (message: Message, ack) => { + uid: "UID", + socketId: socket.id, + displayName: "DISPLAY NAME", + userIcon: { + foregroundImage: "FOREGROUND IMG", + backgroundImage: "BACKGROUND IMG", + }, + location: { + lat: 9999, + lon: 9999, + geohash: "F", + }, + }; // TODO: Send this info from client on connection + await createUser(defaultConnectedUser); + await toggleUserConnectionStatus(socket.id); + + socket.on("disconnect", () => { + console.log(`[WS] User <${socket.id}> exited.`); + deleteConnectedUserByUID(socket.id); + }); + socket.on("ping", (ack) => { + // The (ack) parameter stands for "acknowledgement." This function sends a message back to the originating socket. + console.log(`[WS] Recieved ping from user <${socket.id}>.`); + if (ack) ack("pong"); + }); + socket.on("message", async (message: Message, ack) => { // message post - when someone sends a message - - console.log(`[WS] Recieved message from user <${socket.id}>.`) - console.log(message) - try { - if(isNaN(message.timeSent)) throw new Error("The timeSent parameter must be a valid number.") - if(isNaN(message.location.lat)) throw new Error("The lat parameter must be a valid number.") - if(isNaN(message.location.lon)) throw new Error("The lon parameter must be a valid number.") - if (message.location.geohash == undefined || message.location.geohash === "") { - message.location.geohash = geohashForLocation([Number(message.location.lat), Number(message.location.lon)]) - console.log(`New geohash generated: ${message.location.geohash}`) + console.log(`[WS] Recieved message from user <${socket.id}>.`); + console.log(message); + try { + if (isNaN(message.timeSent)) + throw new Error("The timeSent parameter must be a valid number."); + if (isNaN(message.location.lat)) + throw new Error("The lat parameter must be a valid number."); + if (isNaN(message.location.lon)) + throw new Error("The lon parameter must be a valid number."); + + if ( + message.location.geohash == undefined || + message.location.geohash === "" + ) { + message.location.geohash = geohashForLocation([ + Number(message.location.lat), + Number(message.location.lon), + ]); + console.log(`New geohash generated: ${message.location.geohash}`); } - const status = await createMessage(message); - if(status === false) throw new Error("Error creating message: ") + const status = await createMessage(message); + if (status === false) throw new Error("Error creating message: "); // Get nearby users and push the message to them. - const nearbyUserSockets = await findNearbyUsers(Number(message.location.lat), Number(message.location.lon), Number(process.env.message_outreach_radius)) + const nearbyUserSockets = await findNearbyUsers( + Number(message.location.lat), + Number(message.location.lon), + Number(process.env.message_outreach_radius) + ); for (const recievingSocket of nearbyUserSockets) { // Don't send the message to the sender (who will be included in list of nearby users). if (recievingSocket === socket.id) { - continue + continue; } else { - console.log(`Sending new message to socket ${recievingSocket}`) - socket.broadcast.to(recievingSocket).emit("message", message) + console.log(`Sending new message to socket ${recievingSocket}`); + socket.broadcast.to(recievingSocket).emit("message", message); } } - if (ack) ack("message recieved") - - } catch(error) { - console.error("[WS] Error sending message:", error.message) + if (ack) ack("message recieved"); + } catch (error) { + console.error("[WS] Error sending message:", error.message); } - }) - socket.on('updateLocation', async (location, ack) => { - console.log(`[WS] Recieved new location from user <${socket.id}>.`) + }); + socket.on("updateLocation", async (location, ack) => { + console.log(`[WS] Recieved new location from user <${socket.id}>.`); try { - const lat = Number(location.lat) - const lon = Number(location.lon) - const success = await updateUserLocation(socket.id, lat, lon) + const lat = Number(location.lat); + const lon = Number(location.lon); + const success = await updateUserLocation(socket.id, lat, lon); if (success) { - console.log("[WS] Location updated in database successfully.") - if (ack) ack("location updated") + console.log("[WS] Location updated in database successfully."); + if (ack) ack("location updated"); } else { - throw new Error("updateUserLocation() failed.") + throw new Error("updateUserLocation() failed."); } } catch (error) { - console.error("[WS] Error calling updateLocation:", error.message) + console.error("[WS] Error calling updateLocation:", error.message); } - }) -}) + }); +}); socketServer.listen(socket_port, () => { - console.log(`[WS] Listening for new connections on port ${socket_port}.`) -}) + console.log(`[WS] Listening for new connections on port ${socket_port}.`); +}); // === REST APIs === -app.get('/', (req, res) => { - res.send("Echologator API") -}) +app.get("/", (req, res) => { + res.send("Echologator API"); +}); -app.get('/users', async (req, res) => { - let query = '' +app.get("/users", async (req, res) => { + let query = ""; try { if (req.query.lat && req.query.lon && req.query.radius) { // Looks up all users close to a geographic location extended by a radius (in meters). - query = "?lat&lon&radius" - - const lat = Number(req.query.lat) - const lon = Number(req.query.lon) - const radius = Number(req.query.radius) - - const userIds = await findNearbyUsers(lat, lon, radius) - console.log(userIds) - res.json(userIds) + query = "?lat&lon&radius"; + + const lat = Number(req.query.lat); + const lon = Number(req.query.lon); + const radius = Number(req.query.radius); + + const userIds = await findNearbyUsers(lat, lon, radius); + console.log(userIds); + res.json(userIds); } - } catch(error) { - console.error(`[EXP] Error returning request :\n`, error.message) - res.json(`Operation failed.`) + } catch (error) { + console.error( + `[EXP] Error returning request :\n`, + error.message + ); + res.json(`Operation failed.`); } -}) +}); -app.post('/users', async (req, res) => { - try { - const status = await createUser({ - uid: req.body.uid, - socketId: req.body.socketId, - displayName: req.body.displayName, - userIcon: { - foregroundImage: req.body.userIcon.foregroundImage, - backgroundImage: req.body.userIcon.backgroundImage, - }, - location: { - lat: Number(req.body.location.lat), - lon: Number(req.body.location.lon), - geohash: req.body.location.geohash, - } - }) - if (status === false) throw new Error("Error creating user: ") - res.json("Operation was handled successfully.") - console.log("[EXP] Request returned successfully.") - } catch (error) { - console.error("[EXP] Error returning request :\n", error.message) - res.json(`Operation failed.`) - } -}) +app.post("/users", async (req, res) => { + try { + const status = await createUser({ + uid: req.body.uid, + socketId: req.body.socketId, + displayName: req.body.displayName, + userIcon: { + foregroundImage: req.body.userIcon.foregroundImage, + backgroundImage: req.body.userIcon.backgroundImage, + }, + location: { + lat: Number(req.body.location.lat), + lon: Number(req.body.location.lon), + geohash: req.body.location.geohash, + }, + }); + if (status === false) throw new Error("Error creating user: "); + res.json("Operation was handled successfully."); + console.log("[EXP] Request returned successfully."); + } catch (error) { + console.error( + "[EXP] Error returning request :\n", + error.message + ); + res.json(`Operation failed.`); + } +}); -app.put('/users', async (req, res) => { - let query = "" +app.put("/users", async (req, res) => { + let query = ""; try { if (req.query.userId && req.query.toggleConnection) { // Note: toggleConnection should be assigned 'true', but it at least needs to contain any value. We don't perform a check on this parameter for this reason. - query = "?userId&toggleConnection" - const userId = req.query.userId - if (typeof userId != "string") throw Error(" [userId] is not a string.") - - const success = await toggleUserConnectionStatus(userId) - if (!success) throw Error(" toggleUserConnectionStatus() failed.") - } - else if(req.query.userId && req.query.lat && req.query.lon) { - query = "?userId&lat&lon" - const userId = req.query.userId - const lat = Number(req.query.lat) - const lon = Number(req.query.lon) - if (typeof userId != "string") throw Error(" [userId] is not a string.") - if (typeof lat != "number") throw Error(" [lat] is not a number.") - if (typeof lon != "number") throw Error(" [lon] is not a number.") - - const success = await updateUserLocation(userId, lat, lon) - if (!success) throw Error(" toggleUserConnectionStatus() failed.") + query = "?userId&toggleConnection"; + const userId = req.query.userId; + if (typeof userId != "string") throw Error(" [userId] is not a string."); + + const success = await toggleUserConnectionStatus(userId); + if (!success) throw Error(" toggleUserConnectionStatus() failed."); + } else if (req.query.userId && req.query.lat && req.query.lon) { + query = "?userId&lat&lon"; + const userId = req.query.userId; + const lat = Number(req.query.lat); + const lon = Number(req.query.lon); + if (typeof userId != "string") throw Error(" [userId] is not a string."); + if (typeof lat != "number") throw Error(" [lat] is not a number."); + if (typeof lon != "number") throw Error(" [lon] is not a number."); + + const success = await updateUserLocation(userId, lat, lon); + if (!success) throw Error(" toggleUserConnectionStatus() failed."); } - console.log(`[EXP] Request returned successfully.`) - res.json(`Operation was handled successfully.`) - + console.log(`[EXP] Request returned successfully.`); + res.json(`Operation was handled successfully.`); } catch (error) { - console.error(`[EXP] Error returning request :\n`, error.message) - res.json(`Operation failed.`) + console.error( + `[EXP] Error returning request :\n`, + error.message + ); + res.json(`Operation failed.`); } -}) +}); -app.delete('/users', async (req, res) => { - let query = "" +app.delete("/users", async (req, res) => { + let query = ""; try { - query = "?userId" - const userId = req.query.userId - if (typeof userId != "string") throw Error(" [userId] is not a string.") + query = "?userId"; + const userId = req.query.userId; + if (typeof userId != "string") throw Error(" [userId] is not a string."); + + const success = await deleteConnectedUserByUID(userId); + if (!success) throw Error(" deleteUserById() failed."); - const success = await deleteConnectedUserByUID(userId) - if (!success) throw Error(" deleteUserById() failed.") + console.log(`[EXP] Request returned successfully.`); + res.json(`Operation was handled successfully.`); + } catch (error) { + console.error( + `[EXP] Error returning request :\n`, + error.message + ); + res.json(`Operation failed.`); + } +}); - console.log(`[EXP] Request returned successfully.`) - res.json(`Operation was handled successfully.`) +app.post("/verify", async (req, res) => { + let query = ""; + try { + if (req.query.email) { + query = "?email"; + const email = req.query.email; + const mailgun = new Mailgun(FormData); + const mg = mailgun.client({ + username: "api", + key: process.env.MAILGUN_API_KEY || "key-yourkeyhere", + }); + const data = { + from: "Mailgun Sandbox ", + to: email, + subject: "Verify your email for echologator", + template: "app email verification" + }; + const verifyEmailResponse = await mg.messages.create("sandboxf8629624c26849cf8546cd0bc01ee862.mailgun.org", data) + console.log(`[EXP] Request returned successfully.`); + res.json(verifyEmailResponse); + } } catch (error) { - console.error(`[EXP] Error returning request :\n`, error.message) - res.json(`Operation failed.`) + console.error( + `[EXP] Error returning request :\n`, + error + ); + res.json(`Operation failed.`); } -}) +}); // Error handling -app.get('*', (req, res) => { - res.json("404: Path could not be found! COULD NOT {GET}") - res.status(404) -}) - -app.post('*', (req, res) => { - res.json("404: Path could not be found! COULD NOT {POST}") - res.status(404) -}) - -app.put('*', (req, res) => { - res.json("404: Path could not be found! COULD NOT {PUT}") - res.status(404) -}) - -app.delete('*', (req, res) => { - res.json("404: Path could not be found! COULD NOT {DELETE}") - res.status(404) -}) +app.get("*", (req, res) => { + res.json("404: Path could not be found! COULD NOT {GET}"); + res.status(404); +}); + +app.post("*", (req, res) => { + res.json("404: Path could not be found! COULD NOT {POST}"); + res.status(404); +}); + +app.put("*", (req, res) => { + res.json("404: Path could not be found! COULD NOT {PUT}"); + res.status(404); +}); + +app.delete("*", (req, res) => { + res.json("404: Path could not be found! COULD NOT {DELETE}"); + res.status(404); +}); app.listen(express_port, () => { - return console.log(`[EXP] Listening for requests at http://localhost:${express_port}.`) -}) + return console.log( + `[EXP] Listening for requests at http://localhost:${express_port}.` + ); +}); // Some old API routes are commented out for now due to breaking type changes. @@ -347,4 +401,3 @@ app.listen(express_port, () => { // res.json(false) // } // }) -