diff --git a/functions/package-lock.json b/functions/package-lock.json index b28a48f9..fc1470bf 100644 --- a/functions/package-lock.json +++ b/functions/package-lock.json @@ -10,7 +10,7 @@ "archiver": "^6.0.1", "expo-server-sdk": "^3.7.0", "firebase-admin": "^12.0.0", - "firebase-functions": "^5.0.1" + "firebase-functions": "^5.1.0" }, "devDependencies": { "@types/archiver": "^5.3.3", @@ -256,9 +256,10 @@ } }, "node_modules/@grpc/grpc-js": { - "version": "1.9.13", - "resolved": "https://registry.npmjs.org/@grpc/grpc-js/-/grpc-js-1.9.13.tgz", - "integrity": "sha512-OEZZu9v9AA+7/tghMDE8o5DAMD5THVnwSqDWuh7PPYO5287rTyqy0xEHT6/e4pbqSrhyLPdQFsam4TwFQVVIIw==", + "version": "1.9.15", + "resolved": "https://registry.npmjs.org/@grpc/grpc-js/-/grpc-js-1.9.15.tgz", + "integrity": "sha512-nqE7Hc0AzI+euzUwDAy0aY5hCp10r734gMGRdU+qOPX0XSceI2ULrcXB5U2xSc5VkWwalCj4M7GzCAygZl2KoQ==", + "license": "Apache-2.0", "optional": true, "dependencies": { "@grpc/proto-loader": "^0.7.8", @@ -1136,12 +1137,13 @@ } }, "node_modules/body-parser": { - "version": "1.20.1", - "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.20.1.tgz", - "integrity": "sha512-jWi7abTbYwajOytWCQc37VulmWiRae5RyTpaCyDcS5/lMdtwSz5lOpDE67srw/HYe35f1z3fDQw+3txg7gNtWw==", + "version": "1.20.2", + "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.20.2.tgz", + "integrity": "sha512-ml9pReCu3M61kGlqoTm2umSXTlRTuGTx0bfYj+uIUKKYycG5NtSbeetV3faSU6R7ajOPw0g/J1PvK4qNy7s5bA==", + "license": "MIT", "dependencies": { "bytes": "3.1.2", - "content-type": "~1.0.4", + "content-type": "~1.0.5", "debug": "2.6.9", "depd": "2.0.0", "destroy": "1.2.0", @@ -1149,7 +1151,7 @@ "iconv-lite": "0.4.24", "on-finished": "2.4.1", "qs": "6.11.0", - "raw-body": "2.5.1", + "raw-body": "2.5.2", "type-is": "~1.6.18", "unpipe": "1.0.0" }, @@ -1162,6 +1164,7 @@ "version": "2.6.9", "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "license": "MIT", "dependencies": { "ms": "2.0.0" } @@ -1169,7 +1172,8 @@ "node_modules/body-parser/node_modules/ms": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", - "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==" + "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==", + "license": "MIT" }, "node_modules/brace-expansion": { "version": "1.1.11", @@ -1182,12 +1186,13 @@ } }, "node_modules/braces": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.2.tgz", - "integrity": "sha512-b8um+L1RzM3WDSzvhm6gIz1yfTbBt6YTlcEKAvsmqCZZFw46z626lVj9j1yEPW33H5H+lBQpZMP1k8l+78Ha0A==", + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.3.tgz", + "integrity": "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==", "dev": true, + "license": "MIT", "dependencies": { - "fill-range": "^7.0.1" + "fill-range": "^7.1.1" }, "engines": { "node": ">=8" @@ -1210,6 +1215,7 @@ "version": "3.1.2", "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.2.tgz", "integrity": "sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg==", + "license": "MIT", "engines": { "node": ">= 0.8" } @@ -1340,14 +1346,16 @@ "version": "1.0.5", "resolved": "https://registry.npmjs.org/content-type/-/content-type-1.0.5.tgz", "integrity": "sha512-nTjqfcBFEipKdXCv4YDQWCfmcLZKm81ldF0pAopTvyrFGVbcR6P/VAAd5G7N+0tTr8QqiU0tFadD6FK4NtJwOA==", + "license": "MIT", "engines": { "node": ">= 0.6" } }, "node_modules/cookie": { - "version": "0.5.0", - "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.5.0.tgz", - "integrity": "sha512-YZ3GUyn/o8gfKJlnlX7g7xq4gyO6OSuhGPKaaGssGB2qgDUS0gPgtTvoyZLTt9Ab6dC4hfc9dV5arkvc/OCmrw==", + "version": "0.6.0", + "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.6.0.tgz", + "integrity": "sha512-U71cyTamuh1CRNCfpGY6to28lxvNwPG4Guz/EVjgf3Jmzv0vlDp1atT9eS5dDjMYHucpHbWns6Lwf3BKz6svdw==", + "license": "MIT", "engines": { "node": ">= 0.6" } @@ -2001,16 +2009,17 @@ } }, "node_modules/express": { - "version": "4.18.2", - "resolved": "https://registry.npmjs.org/express/-/express-4.18.2.tgz", - "integrity": "sha512-5/PsL6iGPdfQ/lKM1UuielYgv3BUoJfz1aUwU9vHZ+J7gyvwdQXFEBIEIaxeGf0GIcreATNyBExtalisDbuMqQ==", + "version": "4.19.2", + "resolved": "https://registry.npmjs.org/express/-/express-4.19.2.tgz", + "integrity": "sha512-5T6nhjsT+EOMzuck8JjBHARTHfMht0POzlA60WV2pMD3gyXw2LZnZ+ueGdNxG+0calOJcWKbpFcuzLZ91YWq9Q==", + "license": "MIT", "dependencies": { "accepts": "~1.3.8", "array-flatten": "1.1.1", - "body-parser": "1.20.1", + "body-parser": "1.20.2", "content-disposition": "0.5.4", "content-type": "~1.0.4", - "cookie": "0.5.0", + "cookie": "0.6.0", "cookie-signature": "1.0.6", "debug": "2.6.9", "depd": "2.0.0", @@ -2111,9 +2120,9 @@ "dev": true }, "node_modules/fast-xml-parser": { - "version": "4.3.2", - "resolved": "https://registry.npmjs.org/fast-xml-parser/-/fast-xml-parser-4.3.2.tgz", - "integrity": "sha512-rmrXUXwbJedoXkStenj1kkljNF7ugn5ZjR9FJcwmCfcCbtOMDghPajbc+Tck6vE6F5XsDmx+Pr2le9fw8+pXBg==", + "version": "4.4.1", + "resolved": "https://registry.npmjs.org/fast-xml-parser/-/fast-xml-parser-4.4.1.tgz", + "integrity": "sha512-xkjOecfnKGkSsOwtZ5Pz7Us/T6mrbPQrq0nh+aCO5V9nk5NLWmasAHumTKjiPJPWANe+kAZ84Jc8ooJkzZ88Sw==", "funding": [ { "type": "github", @@ -2124,6 +2133,7 @@ "url": "https://paypal.me/naturalintelligence" } ], + "license": "MIT", "dependencies": { "strnum": "^1.0.5" }, @@ -2164,10 +2174,11 @@ } }, "node_modules/fill-range": { - "version": "7.0.1", - "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.0.1.tgz", - "integrity": "sha512-qOo9F+dMUmC2Lcb4BbVvnKJxTPjCm+RRpe4gDuGrzkL7mEVl/djYSu2OdQ2Pa302N4oqkSg9ir6jaLWJ2USVpQ==", + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.1.1.tgz", + "integrity": "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==", "dev": true, + "license": "MIT", "dependencies": { "to-regex-range": "^5.0.1" }, @@ -2244,9 +2255,10 @@ } }, "node_modules/firebase-functions": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/firebase-functions/-/firebase-functions-5.0.1.tgz", - "integrity": "sha512-1m+crtgAR8Tl36gjpM02KCY5zduAejFmDSXvih/DB93apg39f0U/WwRgT7sitGIRqyCcIpktNUbXJv7Y9JOF4A==", + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/firebase-functions/-/firebase-functions-5.1.0.tgz", + "integrity": "sha512-VO46n9lqljrNiqOv4BbnFHYxY+yYCdZcOeUIF1t9DbFxBbVPztHdMM9MvpfCDp0nzXP2PugdmghSgM0hORrNvw==", + "license": "MIT", "dependencies": { "@types/cors": "^2.8.5", "@types/express": "4.17.3", @@ -2734,6 +2746,7 @@ "version": "0.4.24", "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.24.tgz", "integrity": "sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==", + "license": "MIT", "dependencies": { "safer-buffer": ">= 2.1.2 < 3" }, @@ -2939,6 +2952,7 @@ "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz", "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==", "dev": true, + "license": "MIT", "engines": { "node": ">=0.12.0" } @@ -3076,9 +3090,10 @@ "dev": true }, "node_modules/jose": { - "version": "4.14.4", - "resolved": "https://registry.npmjs.org/jose/-/jose-4.14.4.tgz", - "integrity": "sha512-j8GhLiKmUAh+dsFXlX1aJCbt5KMibuKb+d7j1JaOJG6s2UjX1PQlW+OKB/sD4a/5ZYF4RcmYmLSndOoU3Lt/3g==", + "version": "4.15.9", + "resolved": "https://registry.npmjs.org/jose/-/jose-4.15.9.tgz", + "integrity": "sha512-1vUQX+IdDMVPj4k8kOxgUqlcK518yluMuGZwqlr44FS1ppZB/5GWh4rZG89erpOBOJjU/OBsnCVFfapsRz6nEA==", + "license": "MIT", "funding": { "url": "https://github.com/sponsors/panva" } @@ -3336,6 +3351,7 @@ "version": "0.3.0", "resolved": "https://registry.npmjs.org/media-typer/-/media-typer-0.3.0.tgz", "integrity": "sha512-dq+qelQ9akHpcOl/gUVRTxVIOkAJ1wR3QAvb4RsVjS8oVoFjDGTc679wJYmUmknUF5HwMLOgb5O+a3KxfWapPQ==", + "license": "MIT", "engines": { "node": ">= 0.6" } @@ -3363,12 +3379,13 @@ } }, "node_modules/micromatch": { - "version": "4.0.5", - "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.5.tgz", - "integrity": "sha512-DMy+ERcEW2q8Z2Po+WNXuw3c5YaUSFjAO5GsJqfEl7UjvtIuFKO6ZrKvcItdy98dwFI2N1tg3zNIdKaQT+aNdA==", + "version": "4.0.8", + "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.8.tgz", + "integrity": "sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA==", "dev": true, + "license": "MIT", "dependencies": { - "braces": "^3.0.2", + "braces": "^3.0.3", "picomatch": "^2.3.1" }, "engines": { @@ -3832,6 +3849,7 @@ "version": "6.11.0", "resolved": "https://registry.npmjs.org/qs/-/qs-6.11.0.tgz", "integrity": "sha512-MvjoMCJwEarSbUYk5O+nmoSzSutSsTwF85zcHPQ9OrlFoZOYIjaqBAJIqIXjptyD5vThxGq52Xu/MaJzRkIk4Q==", + "license": "BSD-3-Clause", "dependencies": { "side-channel": "^1.0.4" }, @@ -3876,9 +3894,10 @@ } }, "node_modules/raw-body": { - "version": "2.5.1", - "resolved": "https://registry.npmjs.org/raw-body/-/raw-body-2.5.1.tgz", - "integrity": "sha512-qqJBtEyVgS0ZmPGdCFPWJ3FreoqvG4MVQln/kCgF7Olq95IbOp0/BWyMwbdtn4VTvkM8Y7khCQ2Xgk/tcrCXig==", + "version": "2.5.2", + "resolved": "https://registry.npmjs.org/raw-body/-/raw-body-2.5.2.tgz", + "integrity": "sha512-8zGqypfENjCIqGhgXToC8aB2r7YrBX+AQAfIPs/Mlk+BtPTztOvTS01NRW/3Eh60J+a48lt8qsCzirQ6loCVfA==", + "license": "MIT", "dependencies": { "bytes": "3.1.2", "http-errors": "2.0.0", @@ -4105,7 +4124,8 @@ "node_modules/safer-buffer": { "version": "2.1.2", "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", - "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==" + "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==", + "license": "MIT" }, "node_modules/semver": { "version": "7.5.4", @@ -4431,6 +4451,7 @@ "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==", "dev": true, + "license": "MIT", "dependencies": { "is-number": "^7.0.0" }, @@ -4538,6 +4559,7 @@ "version": "1.6.18", "resolved": "https://registry.npmjs.org/type-is/-/type-is-1.6.18.tgz", "integrity": "sha512-TkRKr9sUTxEH8MdfuCSP7VizJyzRNMjj2J2do2Jr3Kym598JVdEksuzPQCnlFPW4ky9Q+iA+ma9BGm06XQBy8g==", + "license": "MIT", "dependencies": { "media-typer": "0.3.0", "mime-types": "~2.1.24" diff --git a/functions/package.json b/functions/package.json index 4fbe6829..30eb7471 100644 --- a/functions/package.json +++ b/functions/package.json @@ -19,7 +19,7 @@ "archiver": "^6.0.1", "expo-server-sdk": "^3.7.0", "firebase-admin": "^12.0.0", - "firebase-functions": "^5.0.1" + "firebase-functions": "^5.1.0" }, "devDependencies": { "@types/archiver": "^5.3.3", diff --git a/functions/src/events.ts b/functions/src/events.ts index 61238b24..ebcb1803 100644 --- a/functions/src/events.ts +++ b/functions/src/events.ts @@ -50,16 +50,25 @@ export const eventSignIn = functions.https.onCall(async (data, context) => { const uid = data.uid || context.auth?.uid; if (!context.auth) { throw new functions.https.HttpsError("unauthenticated", "Function cannot be called without authentication."); - } else if (typeof data !== "object" || typeof data.eventID !== "string" || typeof data.location !== "object") { + } else if (typeof data !== "object" || typeof data.eventID !== "string" || typeof uid !== "string" || typeof data.location !== "object") { throw new functions.https.HttpsError("invalid-argument", "Invalid data types passed into function"); } + // Only allow privileged users to sign-in for other users + const token = context.auth.token; + if (uid !== context.auth.uid && (token.admin !== true && token.officer !== true && token.developer !== true && token.secretary !== true && token.lead !== true && token.representative !== true)) { + functions.logger.warn(`${context.auth.token} attempted to sign in as ${uid} with invalid permissions.`); + throw new functions.https.HttpsError("permission-denied", `Invalid credentials`); + } + const eventDocRef = db.collection("events").doc(data.eventID); const event: SHPEEvent | undefined = (await eventDocRef.get()).data(); if (typeof event !== "object") { - throw new functions.https.HttpsError("not-found", `Event with id ${data.eventID} could not be found`); + const message = `Event with id ${data.eventID} could not be found`; + functions.logger.log(message); + throw new functions.https.HttpsError("not-found", message); } - console.info(data.location); + // Used to check if user has already signed into event const eventLogDocRef = db.collection(`events/${data.eventID}/logs`).doc(uid); const eventLog: SHPEEventLog = (await eventLogDocRef.get()).data() ?? { @@ -69,30 +78,27 @@ export const eventSignIn = functions.https.onCall(async (data, context) => { verified: true, }; + // Check for any possible errors on the user's part if (eventLog !== undefined && eventLog.signInTime !== undefined) { - const message = "Sign in time already exists."; - functions.logger.error(message); - throw new functions.https.HttpsError("already-exists", message); + functions.logger.error(`User ${uid} attempted sign-in again`); + throw new functions.https.HttpsError("already-exists", "Sign in time already exists."); } else if (event.endTime && (event.endTime.toMillis() + (event.endTimeBuffer ?? 0)) < Date.now()) { - const message = "Event has already ended."; - functions.logger.error(message); - throw new functions.https.HttpsError("deadline-exceeded", message); + functions.logger.error(`User ${uid} has attempted sign-in after event named ${event.name} ended`); + throw new functions.https.HttpsError("deadline-exceeded", "Event has already ended."); } else if (event.startTime && (event.startTime.toMillis() - (event.startTimeBuffer ?? 0) > Date.now())) { - const message = "Event has not started."; - functions.logger.error(message) - throw new functions.https.HttpsError("failed-precondition", message); + functions.logger.error(`User ${uid} has attempted sign-in before event named ${event.name} started`); + throw new functions.https.HttpsError("failed-precondition", "Event has not started."); } else if (event.geolocation && event.geofencingRadius) { - if (typeof data.location.latitude != "number" || typeof data.location.longitude != "number" || !isFinite(data.location.latitude) || !isFinite(data.location.longitude)) { - const message = "Invalid geopoint object passed into function."; - functions.logger.error(message); - throw new functions.https.HttpsError("invalid-argument", message); + if (!data.location || typeof data.location.latitude != "number" || typeof data.location.longitude != "number" || !isFinite(data.location.latitude) || !isFinite(data.location.longitude)) { + functions.logger.error(`User ${uid} has passed an invalid location object: ${data.location}`); + throw new functions.https.HttpsError("invalid-argument", "Invalid geopoint object passed into function."); } const distance = geographicDistance(event.geolocation, data.location); - const message = `${event.name} has geofencing enabled and the given user is ${distance} meters away when required radius is ${event.geofencingRadius} + ${ACCEPTABLE_DISTANCE_ERROR} meters.`; + const message = `${event.name} has geofencing enabled and the given user (${uid}) is reported as ${distance} meters away when required radius is ${event.geofencingRadius} + ${ACCEPTABLE_DISTANCE_ERROR} meters.`; if (distance > event.geofencingRadius + ACCEPTABLE_DISTANCE_ERROR) { functions.logger.error(message); throw new functions.https.HttpsError("out-of-range", message); @@ -117,6 +123,7 @@ export const eventSignIn = functions.https.onCall(async (data, context) => { // Sets log in both event and user collection and ensures both happen by the end of the function. await eventLogDocRef.set(eventLog, { merge: true }); await db.collection(`users/${uid}/event-logs`).doc(data.eventID).set(eventLog, { merge: true }); + functions.logger.log(`User ${uid} successfully signed in and earned ${eventLog.points} points`); return { success: true }; }); @@ -127,17 +134,25 @@ export const eventSignIn = functions.https.onCall(async (data, context) => { */ export const eventSignOut = functions.https.onCall(async (data, context) => { const uid = data.uid || context.auth?.uid; - if (!context.auth) { throw new functions.https.HttpsError("unauthenticated", "Function cannot be called without authentication."); - } else if (typeof data !== "object" || typeof data.eventID !== "string" || typeof data.location !== "object") { + } else if (typeof data !== "object" || typeof data.eventID !== "string" || typeof uid !== "string" || typeof data.location !== "object") { throw new functions.https.HttpsError("invalid-argument", "Invalid data types passed into function"); } + // Only allow privileged users to sign-out for other users + const token = context.auth.token; + if (uid !== context.auth.uid && (token.admin !== true && token.officer !== true && token.developer !== true && token.secretary !== true && token.lead !== true && token.representative !== true)) { + functions.logger.warn(`${context.auth.token} attempted to sign in as ${uid} with invalid permissions.`); + throw new functions.https.HttpsError("permission-denied", `Invalid credentials`); + } + const eventDocRef = db.collection("events").doc(data.eventID); const event: SHPEEvent | undefined = (await eventDocRef.get()).data(); if (typeof event !== "object") { - throw new functions.https.HttpsError("not-found", `Event with id ${data.eventID} could not be found`); + const message = `Event with id ${data.eventID} could not be found`; + functions.logger.log(message); + throw new functions.https.HttpsError("not-found", message); } // Used to check if user has already signed into event @@ -149,22 +164,27 @@ export const eventSignOut = functions.https.onCall(async (data, context) => { verified: true, }; + // Check for any possible errors on the user's part if (eventLog !== undefined && eventLog.signOutTime !== undefined) { + functions.logger.error(`User ${uid} attempted sign-out again`); throw new functions.https.HttpsError("already-exists", "Sign out time already exists."); } else if (event.endTime && (event.endTime.toMillis() + (event.endTimeBuffer ?? 0)) < Date.now()) { + functions.logger.error(`User ${uid} has attempted sign-out after event named ${event.name} ended`); throw new functions.https.HttpsError("deadline-exceeded", "Event has already ended."); } else if (event.startTime && (event.startTime.toMillis() - (event.startTimeBuffer ?? 0) > Date.now())) { - throw new functions.https.HttpsError("failed-precondition", "Event has not started.") + functions.logger.error(`User ${uid} has attempted sign-out before event named ${event.name} started`); + throw new functions.https.HttpsError("failed-precondition", "Event has not started."); } else if (event.geolocation && event.geofencingRadius) { - if (typeof data.location.latitude != "number" || typeof data.location.longitude != "number" || !isFinite(data.location.latitude) || !isFinite(data.location.longitude)) { + if (!data.location || typeof data.location.latitude != "number" || typeof data.location.longitude != "number" || !isFinite(data.location.latitude) || !isFinite(data.location.longitude)) { + functions.logger.error(`User ${uid} has passed an invalid location object: ${data.location}`); throw new functions.https.HttpsError("invalid-argument", "Invalid geopoint object passed into function."); } const distance = geographicDistance(event.geolocation, data.location); - const message = `${event.name} has geofencing enabled and the given user is ${distance} meters away when required radius is ${event.geofencingRadius} + ${ACCEPTABLE_DISTANCE_ERROR} meters.`; + const message = `${event.name} has geofencing enabled and the given user (${uid}) is reported as ${distance} meters away when required radius is ${event.geofencingRadius} + ${ACCEPTABLE_DISTANCE_ERROR} meters.`; if (distance > event.geofencingRadius + ACCEPTABLE_DISTANCE_ERROR) { functions.logger.error(message); throw new functions.https.HttpsError("out-of-range", message); @@ -182,6 +202,14 @@ export const eventSignOut = functions.https.onCall(async (data, context) => { if (eventLog.signInTime && eventLog.signInTime.toMillis() < eventLog.signOutTime.toMillis()) { accumulatedPoints = (eventLog.signOutTime.toMillis() - eventLog.signInTime.toMillis()) / MillisecondTimes.HOUR * (event.pointsPerHour ?? 0); } + + // Ensures the points are capped to the amount of hours the event lasts. + // Only do this when endTime and startTime are truthy (They should be, but we have to take into the case where they're not) + if (event.endTime && event.startTime) { + const eventDurationHours = (event.endTime.toMillis() - event.startTime.toMillis()) / MillisecondTimes.HOUR; + accumulatedPoints = Math.min(eventDurationHours * (event.pointsPerHour ?? 0), accumulatedPoints); + } + eventLog.points = (eventLog.points ?? 0) + (event.signOutPoints ?? 0) + accumulatedPoints; break; case "Volunteer Event": @@ -191,8 +219,10 @@ export const eventSignOut = functions.https.onCall(async (data, context) => { eventLog.points = (eventLog.points ?? 0) + (event.signOutPoints ?? 0) + accumulatedPoints; break; case undefined: + functions.logger.error("Event type is undefined. This means that an issue has occurred during event creation/updating."); throw new functions.https.HttpsError("internal", "Event type is undefined. This means that an issue has occurred during event creation/updating."); case null: + functions.logger.error("Event type is undefined. This means that an issue has occurred during event creation/updating."); throw new functions.https.HttpsError("internal", "Event type is null. This means that an issue has occurred during event creation/updating."); default: eventLog.points = (eventLog.points ?? 0) + (event.signOutPoints ?? 0); @@ -202,6 +232,7 @@ export const eventSignOut = functions.https.onCall(async (data, context) => { // Sets log in both event and user collection and ensures both happen by the end of the function. await eventLogDocRef.set(eventLog, { merge: true }); await db.collection(`users/${uid}/event-logs`).doc(data.eventID).set(eventLog, { merge: true }); + functions.logger.log(`User ${uid} successfully signed in and earned ${eventLog.points} points`); return { success: true }; }); @@ -223,7 +254,9 @@ export const addInstagramPoints = functions.https.onCall(async (data, context) = const eventDocRef = db.collection("events").doc(data.eventID); const event: SHPEEvent | undefined = (await eventDocRef.get()).data(); if (typeof event !== "object") { - throw new functions.https.HttpsError("not-found", `Event with id ${data.eventID} could not be found`); + const message = `Event with id ${data.eventID} could not be found`; + functions.logger.log(message); + throw new functions.https.HttpsError("not-found", message); } const eventLogDocRef = db.collection(`events/${data.eventID}/logs`).doc(uid); @@ -244,6 +277,7 @@ export const addInstagramPoints = functions.https.onCall(async (data, context) = await eventLogDocRef.set(eventLog, { merge: true }); await db.collection(`users/${uid}/event-logs`).doc(data.eventID).set(eventLog, { merge: true }); + functions.logger.log(`User ${uid} successfully signed in and earned ${eventLog.points} points`); return { success: true }; });