diff --git a/firebase.json b/firebase.json index ef7b1e870..78cedec97 100644 --- a/firebase.json +++ b/firebase.json @@ -29,7 +29,8 @@ "yarn --cwd \"$RESOURCE_DIR\" lint", "yarn --cwd \"$RESOURCE_DIR\" build" ], - "source": "functions" + "source": "functions", + "runtime": "nodejs18" }, "emulators": { "functions": { @@ -39,10 +40,11 @@ "port": 8080 }, "hosting": { - "port": 5000 + "port": 5005 }, "ui": { "enabled": true } } -} + } + \ No newline at end of file diff --git a/functions/.gitignore b/functions/.gitignore index 5ecd88ed8..3da5a95d1 100644 --- a/functions/.gitignore +++ b/functions/.gitignore @@ -7,3 +7,6 @@ typings/ # Node.js dependency directory node_modules/ + +# local runtime config for Twilio +.runtimeconfig.json \ No newline at end of file diff --git a/functions/package.json b/functions/package.json index 1016a726a..a20a7b61c 100644 --- a/functions/package.json +++ b/functions/package.json @@ -19,6 +19,7 @@ "firebase-admin": "^12.0.0", "firebase-functions": "^4.8.2", "firebase-tools": "11.18.0", + "mailtrap": "^3.3.0", "twilio": "^3.71.3" }, "devDependencies": { diff --git a/functions/src/index.ts b/functions/src/index.ts index ae646340e..3dd52af6a 100644 --- a/functions/src/index.ts +++ b/functions/src/index.ts @@ -1,6 +1,7 @@ import * as functions from 'firebase-functions'; import * as admin from 'firebase-admin'; import { Twilio } from 'twilio'; +import { MailtrapClient } from 'mailtrap'; // Use admin SDK to enable writing to other parts of database // const admin = require('firebase-admin'); @@ -8,17 +9,17 @@ admin.initializeApp(); const db = admin.firestore(); -// Twilio Setup -const accountSid = functions.config().twilio.accountsid; -const authToken = functions.config().twilio.twilio_auth_token; -const twilioNumber = functions.config().twilio.twilionumber; - -const client = new Twilio(accountSid, authToken); - /** - * Function that handles data and sends a text message to a requested phone number - */ +* Function that handles data and sends a text message to a requested phone number +*/ async function sendSMS (user: FireUser, message: string) { + // Twilio Setup + const accountSid = process.env.ACCOUNTSID as string; + const authToken = process.env.TWILIO_AUTH_TOKEN as string; + const twilioNumber = process.env.TWILIO_NUMBER; + + const client = new Twilio(accountSid, authToken); + if(process.env.DATABASE === "staging") { return; } @@ -40,10 +41,10 @@ async function sendSMS (user: FireUser, message: string) { } /** Adds new roles to a user without them being in QMI's system - * Not inclusive: Still need to consider users that are - * already in the system. (THIS CASE IS HANDLED BY NOT INCLDUING THEM IN THE - * pendingUsers COLLECTION IN THE FIRST PLACE) - */ +* Not inclusive: Still need to consider users that are +* already in the system. (THIS CASE IS HANDLED BY NOT INCLDUING THEM IN THE +* pendingUsers COLLECTION IN THE FIRST PLACE) +*/ exports.onUserCreate = functions.firestore .document('users/{userId}') .onCreate(async (snap, context) => { @@ -140,7 +141,7 @@ exports.onCommentCreate = functions.firestore title: 'Student comment', subtitle: "New student comment", message: `${asker.firstName} commented \ - on your assigned question`.trim(), + on your assigned question`.trim(), createdAt: admin.firestore.Timestamp.now() }) }).catch(() => { @@ -149,13 +150,13 @@ exports.onCommentCreate = functions.firestore title: 'Student comment', subtitle: "New student comment", message: `${asker.firstName} commented \ - on your assigned question`.trim(), + on your assigned question`.trim(), createdAt: admin.firestore.Timestamp.now() }], notifications: admin.firestore.Timestamp.now(), productUpdates: admin.firestore.Timestamp.now(), lastSent: admin.firestore.Timestamp.now(),}) - + }); } else { db.doc(`notificationTrackers/${asker.email}`) @@ -177,7 +178,7 @@ exports.onCommentCreate = functions.firestore notifications: admin.firestore.Timestamp.now(), productUpdates: admin.firestore.Timestamp.now(), lastSent: admin.firestore.Timestamp.now(),}) - + }); } }) @@ -201,15 +202,15 @@ exports.onSessionUpdate = functions.firestore const asker: FireUser = (await db.doc(`users/${topQuestion.askerId}`) .get()).data() as FireUser; sendSMS(asker, `Your question has reached the top of the \ - ${sessionName} queue. A TA will likely help you shortly.`); + ${sessionName} queue. A TA will likely help you shortly.`); db.doc(`notificationTrackers/${asker.email}`) .update({ notificationList: admin.firestore.FieldValue.arrayUnion({ title: 'Your Question is Up!', subtitle: `Your question has reached the top of the \ - ${sessionName} queue.`, + ${sessionName} queue.`, message: `Your question has reached the top of the \ - ${sessionName} queue. A TA will likely help you shortly.`, + ${sessionName} queue. A TA will likely help you shortly.`, createdAt: admin.firestore.Timestamp.now() }) }).catch(() => { @@ -217,15 +218,15 @@ exports.onSessionUpdate = functions.firestore notificationList: [{ title: 'Your Question is Up!', subtitle: `Your question has reached the top of the \ - ${sessionName} queue.`, + ${sessionName} queue.`, message: `Your question has reached the top of the \ - ${sessionName} queue. A TA will likely help you shortly.`, + ${sessionName} queue. A TA will likely help you shortly.`, createdAt: admin.firestore.Timestamp.now() }], notifications: admin.firestore.Timestamp.now(), productUpdates: admin.firestore.Timestamp.now(), lastSent: admin.firestore.Timestamp.now(),}) - + }); db.doc(`questions/${afterQuestions[0].id}`).update({ wasNotified: true @@ -266,7 +267,7 @@ exports.onQuestionCreate = functions.firestore notifications: admin.firestore.Timestamp.now(), productUpdates: admin.firestore.Timestamp.now(), lastSent: admin.firestore.Timestamp.now(),}) - + }) }); @@ -291,7 +292,7 @@ exports.onQuestionCreate = functions.firestore notifications: admin.firestore.Timestamp.now(), productUpdates: admin.firestore.Timestamp.now(), lastSent: admin.firestore.Timestamp.now(),}) - + }) }); return db.doc(`sessions/${sessionId}`).update({ @@ -337,15 +338,15 @@ exports.onQuestionUpdate = functions.firestore // Derive timing changes (changes from assigned to unassigned) if (numAssignedChange === 1 && newQuestion.timeAssigned !== undefined) { // Add new time addressed - waitTimeChange = - (newQuestion.timeAssigned.seconds - newQuestion.timeEntered.seconds) - / (newQuestion.position || 1); + waitTimeChange = + (newQuestion.timeAssigned.seconds - newQuestion.timeEntered.seconds) + / (newQuestion.position || 1); } else if (numAssignedChange === -1 && prevQuestion.timeAssigned !== undefined) { // Subtract previous time addressed - waitTimeChange = - (prevQuestion.timeEntered.seconds - prevQuestion.timeAssigned.seconds) - / (newQuestion.position || 1); + waitTimeChange = + (prevQuestion.timeEntered.seconds - prevQuestion.timeAssigned.seconds) + / (newQuestion.position || 1); } // Derive timing changes (changes from assigned to resolved) @@ -353,8 +354,8 @@ exports.onQuestionUpdate = functions.firestore resolveTimeChange = newQuestion.timeAddressed!.seconds - newQuestion.timeAssigned.seconds; } else if (numResolvedChange === -1 - && prevQuestion.timeAssigned !== undefined - && prevQuestion.timeAddressed !== undefined + && prevQuestion.timeAssigned !== undefined + && prevQuestion.timeAddressed !== undefined ) { resolveTimeChange = prevQuestion.timeAssigned.seconds - prevQuestion.timeAddressed.seconds; } @@ -364,7 +365,7 @@ exports.onQuestionUpdate = functions.firestore if ( prevQuestion.answererId !== newQuestion.answererId && - newQuestion.answererId !== '' + newQuestion.answererId !== '' ) { db.doc(`notificationTrackers/${asker.email}`) .update({ @@ -385,13 +386,13 @@ exports.onQuestionUpdate = functions.firestore notifications: admin.firestore.Timestamp.now(), productUpdates: admin.firestore.Timestamp.now(), lastSent: admin.firestore.Timestamp.now(),}) - + }); } if ( prevQuestion.answererId !== newQuestion.answererId && - newQuestion.answererId === '' + newQuestion.answererId === '' ) { const session: FireSession = (await db.doc(`sessions/${sessionId}`).get()).data() as FireSession; db.doc(`notificationTrackers/${asker.email}`) @@ -400,8 +401,8 @@ exports.onQuestionUpdate = functions.firestore title: 'TA Unassigned', subtitle: 'TA Unassigned', message: - `A TA has been unassigned from your question and you have \ - been readded to the top of the ${session.title} queue.`, + `A TA has been unassigned from your question and you have \ + been readded to the top of the ${session.title} queue.`, createdAt: admin.firestore.Timestamp.now() }) }).catch(() => { @@ -410,14 +411,14 @@ exports.onQuestionUpdate = functions.firestore title: 'TA Unassigned', subtitle: 'TA Unassigned', message: - `A TA has been unassigned from your question and you have \ - been readded to the top of the ${session.title} queue.`, + `A TA has been unassigned from your question and you have \ + been readded to the top of the ${session.title} queue.`, createdAt: admin.firestore.Timestamp.now() }], notifications: admin.firestore.Timestamp.now(), productUpdates: admin.firestore.Timestamp.now(), lastSent: admin.firestore.Timestamp.now(),}) - + }); } else if (newQuestion.status === 'resolved') { @@ -428,8 +429,8 @@ exports.onQuestionUpdate = functions.firestore title: 'Question resolved', subtitle: 'Question marked as resolved', message: - `A TA has marked your question as resolved and you \ - have been removed from the ${session.title} queue`, + `A TA has marked your question as resolved and you \ + have been removed from the ${session.title} queue`, createdAt: admin.firestore.Timestamp.now() }) }).catch(() => { @@ -438,14 +439,14 @@ exports.onQuestionUpdate = functions.firestore title: 'Question marked no-show', subtitle: 'Question marked as no-show', message: - `A TA has marked your question as no-show and you \ - have been removed from the ${session.title} queue`, + `A TA has marked your question as no-show and you \ + have been removed from the ${session.title} queue`, createdAt: admin.firestore.Timestamp.now() }], notifications: admin.firestore.Timestamp.now(), productUpdates: admin.firestore.Timestamp.now(), lastSent: admin.firestore.Timestamp.now(),}) - + }); } else if (newQuestion.status === "no-show") { const session: FireSession = (await db.doc(`sessions/${sessionId}`).get()).data() as FireSession; @@ -455,8 +456,8 @@ exports.onQuestionUpdate = functions.firestore title: 'Question marked no-show', subtitle: 'Question marked as no-show', message: - `A TA has marked your question as no-show and you \ - have been removed from the ${session.title} queue`, + `A TA has marked your question as no-show and you \ + have been removed from the ${session.title} queue`, createdAt: admin.firestore.Timestamp.now() }) }).catch(() => { @@ -465,14 +466,14 @@ exports.onQuestionUpdate = functions.firestore title: 'Question marked no-show', subtitle: 'Question marked as no-show', message: - `A TA has marked your question as no-show and you \ - have been removed from the ${session.title} queue`, + `A TA has marked your question as no-show and you \ + have been removed from the ${session.title} queue`, createdAt: admin.firestore.Timestamp.now() }], notifications: admin.firestore.Timestamp.now(), productUpdates: admin.firestore.Timestamp.now(), lastSent: admin.firestore.Timestamp.now(),}) - + }); } @@ -484,4 +485,30 @@ exports.onQuestionUpdate = functions.firestore totalWaitTime: admin.firestore.FieldValue.increment(waitTimeChange), totalResolveTime: admin.firestore.FieldValue.increment(resolveTimeChange), }); - }); \ No newline at end of file + }); + +exports.onPendingUserCreate = functions.firestore + .document('pendingUsers/{userEmail}') + .onCreate(async (snap, context) => { + const userEmail = context.params.userEmail + const mailtrapClient = new MailtrapClient({ token: process.env.MAILTRAP_TOKEN as string }); + const sender = { + email: "mailtrap@queueme.in", + name: "QueueMeIn Team" + } + const recipient = [{ + email: userEmail + }] + + try { + await mailtrapClient.send({ + from: sender, + to: recipient, + subject: "You've been invited to QueueMeIn!", + text: "You've been invited to QueueMeIn! Click here to sign up: https://queueme.in", + category: "QMI Invite – Test", + }); + } catch (error) { + // console.log("error sending mail: ", error) + } + }); diff --git a/functions/yarn.lock b/functions/yarn.lock index 6b483e03a..afd8f2613 100644 --- a/functions/yarn.lock +++ b/functions/yarn.lock @@ -1037,6 +1037,15 @@ aws4@^1.8.0: resolved "https://registry.yarnpkg.com/aws4/-/aws4-1.12.0.tgz#ce1c9d143389679e253b314241ea9aa5cec980d3" integrity sha512-NmWvPnx0F1SfrQbYwOi7OeaNGokp9XhzNioJ/CSBs8Qa4vxug81mhJEAVZwxXuBmYB5KDRfMq/F3RR0BIU7sWg== +axios@>=0.27: + version "1.7.7" + resolved "https://registry.yarnpkg.com/axios/-/axios-1.7.7.tgz#2f554296f9892a72ac8d8e4c5b79c14a91d0a47f" + integrity sha512-S4kL7XrjgBmvdGut0sN3yJxqYzrDOnivkBiN0OFs6hLiUam3UPvswUo0kqGyhqUZGEOytHyumEdXsAkgCOUf3Q== + dependencies: + follow-redirects "^1.15.6" + form-data "^4.0.0" + proxy-from-env "^1.1.0" + axios@^0.21.4: version "0.21.4" resolved "https://registry.yarnpkg.com/axios/-/axios-0.21.4.tgz#c67b90dc0568e5c1cf2b0b858c43ba28e2eda575" @@ -2777,6 +2786,11 @@ follow-redirects@^1.14.0: resolved "https://registry.yarnpkg.com/follow-redirects/-/follow-redirects-1.15.4.tgz#cdc7d308bf6493126b17ea2191ea0ccf3e535adf" integrity sha512-Cr4D/5wlrb0z9dgERpUL3LrmPKVDsETIJhaCMeDfuFYcqa5bldGV6wBsAN6X/vxlXQtFBMrXdXxdL8CbDTGniw== +follow-redirects@^1.15.6: + version "1.15.9" + resolved "https://registry.yarnpkg.com/follow-redirects/-/follow-redirects-1.15.9.tgz#a604fa10e443bf98ca94228d9eebcc2e8a2c8ee1" + integrity sha512-gew4GsXizNgdoRyqmyfMHyAmXsZDk6mHkSxZFCzW9gwlbtOW44CDtYavM+y+72qD/Vq2l550kMF52DT8fOLJqQ== + foreground-child@^3.1.0: version "3.1.1" resolved "https://registry.yarnpkg.com/foreground-child/-/foreground-child-3.1.1.tgz#1d173e776d75d2772fed08efe4a0de1ea1b12d0d" @@ -4329,6 +4343,13 @@ lru-memoizer@^2.2.0: lodash.clonedeep "^4.5.0" lru-cache "~4.0.0" +mailtrap@^3.3.0: + version "3.4.0" + resolved "https://registry.yarnpkg.com/mailtrap/-/mailtrap-3.4.0.tgz#b02d43f81492bc0bf79edd92386ec232dc3074a7" + integrity sha512-gegg90/gMY8hvfxB+WMtE8RRZyhQr90jUw00QOLApIAomItumqFBCpZv5IfG51EUKThu9+p7X4QdNA4buryenw== + dependencies: + axios ">=0.27" + make-dir@^3.0.0: version "3.1.0" resolved "https://registry.yarnpkg.com/make-dir/-/make-dir-3.1.0.tgz#415e967046b3a7f1d185277d84aa58203726a13f" @@ -4416,6 +4437,11 @@ mime-db@1.52.0: resolved "https://registry.yarnpkg.com/mime-db/-/mime-db-1.52.0.tgz#bbabcdc02859f4987301c856e3387ce5ec43bf70" integrity sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg== +"mime-db@>= 1.43.0 < 2": + version "1.53.0" + resolved "https://registry.yarnpkg.com/mime-db/-/mime-db-1.53.0.tgz#3cb63cd820fc29896d9d4e8c32ab4fcd74ccb447" + integrity sha512-oHlN/w+3MQ3rba9rqFr6V/ypF10LSkdwUysQL7GkXoTgIWeV+tcXGA852TBxH+gsh8UWoyhR1hKcoMJTuWflpg== + mime-types@^2.1.12, mime-types@^2.1.16, mime-types@~2.1.19: version "2.1.35" resolved "https://registry.yarnpkg.com/mime-types/-/mime-types-2.1.35.tgz#381a871b62a734450660ae3deee44813f70d959a" @@ -5146,7 +5172,7 @@ proxy-agent@^5.0.0: proxy-from-env "^1.0.0" socks-proxy-agent "^5.0.0" -proxy-from-env@^1.0.0: +proxy-from-env@^1.0.0, proxy-from-env@^1.1.0: version "1.1.0" resolved "https://registry.yarnpkg.com/proxy-from-env/-/proxy-from-env-1.1.0.tgz#e102f16ca355424865755d2c9e8ea4f24d58c3e2" integrity sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg== @@ -5559,7 +5585,7 @@ semver@^5.0.1, semver@^5.5.0, semver@^5.6.0, semver@^5.7.1: resolved "https://registry.yarnpkg.com/semver/-/semver-5.7.2.tgz#48d55db737c3287cd4835e17fa13feace1c41ef8" integrity sha512-cBznnQ9KjJqU67B52RMC65CMarK2600WFnbkcaiwWq3xy/5haFJlshgnpjovMVJ+Hff49d8GEn0b87C5pDQ10g== -semver@^6.2.0, semver@^6.3.0: +semver@^6.0.0, semver@^6.2.0, semver@^6.3.0: version "6.3.1" resolved "https://registry.yarnpkg.com/semver/-/semver-6.3.1.tgz#556d2ef8689146e46dcea4bfdd095f3434dffcb4" integrity sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA== diff --git a/package.json b/package.json index 6c1dc36c7..d4e356fdf 100644 --- a/package.json +++ b/package.json @@ -24,7 +24,7 @@ "chai": "^4.2.0", "firebase": "^8.0.1", "firebase-admin": "^9.3.0", - "firebase-functions": "^3.11.0", + "firebase-functions": "^4.8.2", "history": "^4.10.1", "linkifyjs": "^2.1.9", "lodash": "^4.17.15", @@ -64,7 +64,7 @@ "deploy": "firebase deploy", "start": "NODE_OPTIONS=--openssl-legacy-provider react-scripts start", "s": "yarn start", - "test": "NODE_ENV=test firebase emulators:exec --only firestore \"./test.sh\"", + "test": "NODE_OPTIONS=--openssl-legacy-provider NODE_ENV=test firebase emulators:exec --only firestore \"./test.sh\"", "lint": "eslint . --ext tsx --ext ts", "analyze": "source-map-explorer 'build/static/js/*.js'" }, diff --git a/src/firebasefunctions/importProfessorsOrTAs.ts b/src/firebasefunctions/importProfessorsOrTAs.ts index 24dab3979..de5ad4496 100644 --- a/src/firebasefunctions/importProfessorsOrTAs.ts +++ b/src/firebasefunctions/importProfessorsOrTAs.ts @@ -101,6 +101,7 @@ const importProfessorsOrTAs = async ( }); + // missingSet at this point contains the emails of all users who were not found in the database // add missing user to pendingUsers collection missingSet.forEach(email => { const pendingUsersRef = db.collection('pendingUsers').doc(email); diff --git a/yarn.lock b/yarn.lock index 29f80a4ce..b17974be4 100644 --- a/yarn.lock +++ b/yarn.lock @@ -8342,16 +8342,16 @@ firebase-admin@^9.3.0: "@google-cloud/firestore" "^4.5.0" "@google-cloud/storage" "^5.3.0" -firebase-functions@^3.11.0: - version "3.16.0" - resolved "https://registry.yarnpkg.com/firebase-functions/-/firebase-functions-3.16.0.tgz#603e47c2a563a5d0d1bc28f7362d0349c2f0d33f" - integrity sha512-6ISOn0JckMtpA3aJ/+wCCGhThUhBUrpZD+tSkUeolx0Vr+NoYFXA0+2YzJZa/A2MDU8gotPzUtnauLSEQvfClQ== +firebase-functions@^4.8.2: + version "4.9.0" + resolved "https://registry.yarnpkg.com/firebase-functions/-/firebase-functions-4.9.0.tgz#7b3580a2374dab705f3b7d2a5bf4597489ef74be" + integrity sha512-IqxOEsVAWGcRv9KRGzWQR5mOFuNsil3vsfkRPPiaV1U/ATC27/jbahh4z8I4rW8Xqa6cQE5xqnw0ueyMH7i7Ag== dependencies: "@types/cors" "^2.8.5" "@types/express" "4.17.3" cors "^2.8.5" express "^4.17.1" - lodash "^4.17.14" + protobufjs "^7.2.2" firebase-tools@11.18.0: version "11.18.0" @@ -14337,6 +14337,24 @@ protobufjs@^7.0.0, protobufjs@^7.2.4: "@types/node" ">=13.7.0" long "^5.0.0" +protobufjs@^7.2.2: + version "7.4.0" + resolved "https://registry.yarnpkg.com/protobufjs/-/protobufjs-7.4.0.tgz#7efe324ce9b3b61c82aae5de810d287bc08a248a" + integrity sha512-mRUWCc3KUU4w1jU8sGxICXH/gNS94DvI1gxqDvBzhj1JpcsimQkYiOJfwsPUykUI5ZaspFbSgmBLER8IrQ3tqw== + dependencies: + "@protobufjs/aspromise" "^1.1.2" + "@protobufjs/base64" "^1.1.2" + "@protobufjs/codegen" "^2.0.4" + "@protobufjs/eventemitter" "^1.1.0" + "@protobufjs/fetch" "^1.1.0" + "@protobufjs/float" "^1.0.2" + "@protobufjs/inquire" "^1.1.0" + "@protobufjs/path" "^1.1.2" + "@protobufjs/pool" "^1.1.0" + "@protobufjs/utf8" "^1.1.0" + "@types/node" ">=13.7.0" + long "^5.0.0" + proxy-addr@~2.0.5: version "2.0.7" resolved "https://registry.yarnpkg.com/proxy-addr/-/proxy-addr-2.0.7.tgz#f19fe69ceab311eeb94b42e70e8c2070f9ba1025"