From 75e7f5c073237dd03a9b66932a1ee126c81cebbc Mon Sep 17 00:00:00 2001
From: HeetShah <heetshah7642@gmail.com>
Date: Sun, 12 Nov 2023 00:41:06 -0700
Subject: [PATCH 1/3] Adds sendSignInLink mutation

---
 .gitignore                                    |  1 +
 backend/typescript/graphql/index.ts           |  1 +
 .../graphql/resolvers/authResolvers.ts        | 15 ++++-
 backend/typescript/graphql/types/authType.ts  |  1 +
 backend/typescript/server.ts                  | 63 ++++++++++++++++++-
 .../services/implementations/authService.ts   | 49 +++++++++++++++
 .../services/interfaces/authService.ts        |  8 +++
 7 files changed, 135 insertions(+), 3 deletions(-)

diff --git a/.gitignore b/.gitignore
index fab8049..56e1dce 100644
--- a/.gitignore
+++ b/.gitignore
@@ -13,3 +13,4 @@ backend/typescript/serviceAccount.json
 package-lock.json
 .gitignore
 package.json
+backend/typescript/graphql/sampleData/users.json
diff --git a/backend/typescript/graphql/index.ts b/backend/typescript/graphql/index.ts
index b01f5c7..7c36fcc 100644
--- a/backend/typescript/graphql/index.ts
+++ b/backend/typescript/graphql/index.ts
@@ -89,6 +89,7 @@ const graphQLMiddlewares = {
     deleteUserByEmail: authorizedByAdmin(),
     logout: isAuthorizedByUserId("userId"),
     resetPassword: isAuthorizedByEmail("email"),
+    sendSignInLink: authorizedByAllRoles(),
   },
 };
 
diff --git a/backend/typescript/graphql/resolvers/authResolvers.ts b/backend/typescript/graphql/resolvers/authResolvers.ts
index 69fe31b..fa8a500 100644
--- a/backend/typescript/graphql/resolvers/authResolvers.ts
+++ b/backend/typescript/graphql/resolvers/authResolvers.ts
@@ -1,5 +1,4 @@
 // import { CookieOptions } from "express";
-
 import * as firebaseAdmin from "firebase-admin";
 import nodemailerConfig from "../../nodemailer.config";
 import AuthService from "../../services/implementations/authService";
@@ -15,7 +14,10 @@ import IReviewService from "../../services/interfaces/reviewService";
 import ReviewService from "../../services/implementations/reviewService";
 
 const userService: IUserService = new UserService();
-const emailService: IEmailService = new EmailService(nodemailerConfig);
+const emailService: IEmailService = new EmailService(
+  nodemailerConfig,
+  "UW Blueprint Internal Tools Team",
+);
 const authService: IAuthService = new AuthService(userService, emailService);
 const reviewService: IReviewService = new ReviewService();
 
@@ -142,6 +144,15 @@ const authResolvers = {
       await authService.resetPassword(email);
       return true;
     },
+    sendSignInLink: async (
+      _parent: undefined,
+      { email }: { email: string },
+    ): Promise<boolean> => {
+      await authService.sendSignInLink(email).catch((err) => {
+        throw err;
+      });
+      return true;
+    },
   },
 };
 
diff --git a/backend/typescript/graphql/types/authType.ts b/backend/typescript/graphql/types/authType.ts
index 8ec335c..c9c4992 100644
--- a/backend/typescript/graphql/types/authType.ts
+++ b/backend/typescript/graphql/types/authType.ts
@@ -38,6 +38,7 @@ const authType = gql`
     refresh(refreshToken: String!): String!
     logout(userId: ID!): ID
     resetPassword(email: String!): Boolean!
+    sendSignInLink(email: String!): Boolean!
   }
 `;
 
diff --git a/backend/typescript/server.ts b/backend/typescript/server.ts
index c3eebfe..8a5dee3 100644
--- a/backend/typescript/server.ts
+++ b/backend/typescript/server.ts
@@ -6,6 +6,8 @@ import { ApolloServer } from "apollo-server-express";
 import { sequelize } from "./models";
 import schema from "./graphql";
 import Application from "./models/application.model";
+import memeberData from "./graphql/sampleData/members.json";
+import firebaseAuthUsers from "./graphql/sampleData/users.json";
 
 const CORS_ALLOW_LIST = [
   "http://localhost:3000",
@@ -63,6 +65,65 @@ admin.initializeApp({
 const db = admin.database();
 const ref = db.ref("studentApplications");
 
+app.get("/diff", async (req, res) => {
+  const currentTerm = memeberData.term;
+  const currentTermMembers: string[] = [];
+
+  // const teamToMembers : Record<string, string[]> = {};
+  memeberData.members.forEach((member) => {
+    if (member.term === currentTerm) {
+      currentTermMembers.push(member.name);
+      // if (teamToMembers[member.teams[0]]) {
+      //   teamToMembers[member.teams[0]].push(member.name);
+      // } else {
+      //   teamToMembers[member.teams[0]] = [member.name];
+      // }
+    }
+  });
+
+  // const teamToMemberSize : Record<string, number> = {};
+  // (Object.keys(teamToMembers)).forEach((team) => {
+  //   teamToMemberSize[team] = teamToMembers[team].length;
+  // }
+  // )
+
+  const firebaseUsers: Record<string, string | undefined> = {};
+  firebaseAuthUsers.forEach((user) => {
+    firebaseUsers[user.uid] = user.displayName;
+  });
+
+  // see if all currentTermMembers have their name in firebase_users
+  const missingMembersFromFirebaseAuth: string[] = [];
+
+  currentTermMembers.forEach((member) => {
+    if (!Object.values(firebaseUsers).includes(member)) {
+      missingMembersFromFirebaseAuth.push(member);
+    }
+  });
+
+  res.status(200).json({
+    currentTerm,
+    currentTermMembers,
+    firebaseUsers,
+    missingMembersFromFirebaseAuth,
+  });
+});
+
+app.get("/authUsers", async (req, res) => {
+  try {
+    admin
+      .auth()
+      .listUsers()
+      .then((data) => {
+        res.status(200).json(data.users);
+      });
+  } catch (error) {
+    res
+      .status(500)
+      .send("An error occurred while retrieving the applications.");
+  }
+});
+
 app.get("/termApplications", async (req, res) => {
   ref
     .orderByChild("term")
@@ -103,7 +164,6 @@ app.get("/applications/:id", async (req, res) => {
       res.status(404).send("Student application not found.");
     }
   } catch (error) {
-    console.error(error);
     res
       .status(500)
       .send("An error occurred while retrieving the student application.");
@@ -111,5 +171,6 @@ app.get("/applications/:id", async (req, res) => {
 });
 
 app.listen({ port: process.env.PORT || 5000 }, () => {
+  // eslint-disable-next-line no-console
   console.info(`Server is listening on port ${process.env.PORT || 5000}!`);
 });
diff --git a/backend/typescript/services/implementations/authService.ts b/backend/typescript/services/implementations/authService.ts
index 6f5ce78..4caecd5 100644
--- a/backend/typescript/services/implementations/authService.ts
+++ b/backend/typescript/services/implementations/authService.ts
@@ -226,6 +226,55 @@ class AuthService implements IAuthService {
       return false;
     }
   }
+
+  async sendSignInLink(email: string): Promise<boolean> {
+    if (!this.emailService) {
+      const errorMessage =
+        "Attempted to call sendEmailVerificationLink but this instance of AuthService does not have an EmailService instance";
+      Logger.error(errorMessage);
+      throw new Error(errorMessage);
+    }
+
+    if (!email.endsWith("@uwblueprint.org")) {
+      const errorMessage = `Attempted to call sendEmailVerificationLink with an email, ${email}, that does not end with @uwblueprint.org`;
+      Logger.error(errorMessage);
+      throw new Error(errorMessage);
+    }
+
+    try {
+      await firebaseAdmin
+        .auth()
+        .generateSignInWithEmailLink(email, {
+          url: `${process.env.FIREBASE_REQUEST_URI}/admin`,
+          handleCodeInApp: true,
+        })
+        .then((link) => {
+          const emailBody = `
+          Hello,
+          <br><br>
+          We noticed that you are a current UW Blueprint member but do not have an account in our internal recruitment tool. Please click the following link to sign in with your blueprint email.
+          <br><br>
+          <a href=${link}>Sign into internal recruitment tool</a>`;
+
+          return this.emailService?.sendEmail(
+            email,
+            "Sign into internal recruitment tool",
+            emailBody,
+          );
+        })
+        .catch((error) => {
+          Logger.error("Failed to send email sign in link to user with email");
+          throw error;
+        });
+
+      return true;
+    } catch (error) {
+      Logger.error(
+        `Failed to generate email sign in link for user with email ${email} ${error}`,
+      );
+      throw error;
+    }
+  }
 }
 
 export default AuthService;
diff --git a/backend/typescript/services/interfaces/authService.ts b/backend/typescript/services/interfaces/authService.ts
index 85d4f36..5972d45 100644
--- a/backend/typescript/services/interfaces/authService.ts
+++ b/backend/typescript/services/interfaces/authService.ts
@@ -82,6 +82,14 @@ interface IAuthService {
     accessToken: string,
     requestedEmail: string,
   ): Promise<boolean>;
+
+  /**
+   * Sends an email to the input with a sign in link to the application. This will be used to create a user in firebase for current term BP members who are not in the database.
+   * @param email email of user to be created
+   * @throws Error if unable to generate link or send email
+   * @returns true if email sent successfully
+   */
+  sendSignInLink(email: string): Promise<boolean>;
 }
 
 export default IAuthService;

From ecf6f6c4f8910766d963617ad61e59d641b7c2a2 Mon Sep 17 00:00:00 2001
From: HeetShah <heetshah7642@gmail.com>
Date: Tue, 14 Nov 2023 16:19:35 -0700
Subject: [PATCH 2/3] updates comments

---
 backend/typescript/server.ts | 18 +++---------------
 1 file changed, 3 insertions(+), 15 deletions(-)

diff --git a/backend/typescript/server.ts b/backend/typescript/server.ts
index 8a5dee3..70ba491 100644
--- a/backend/typescript/server.ts
+++ b/backend/typescript/server.ts
@@ -69,24 +69,12 @@ app.get("/diff", async (req, res) => {
   const currentTerm = memeberData.term;
   const currentTermMembers: string[] = [];
 
-  // const teamToMembers : Record<string, string[]> = {};
   memeberData.members.forEach((member) => {
     if (member.term === currentTerm) {
       currentTermMembers.push(member.name);
-      // if (teamToMembers[member.teams[0]]) {
-      //   teamToMembers[member.teams[0]].push(member.name);
-      // } else {
-      //   teamToMembers[member.teams[0]] = [member.name];
-      // }
     }
   });
 
-  // const teamToMemberSize : Record<string, number> = {};
-  // (Object.keys(teamToMembers)).forEach((team) => {
-  //   teamToMemberSize[team] = teamToMembers[team].length;
-  // }
-  // )
-
   const firebaseUsers: Record<string, string | undefined> = {};
   firebaseAuthUsers.forEach((user) => {
     firebaseUsers[user.uid] = user.displayName;
@@ -127,9 +115,9 @@ app.get("/authUsers", async (req, res) => {
 app.get("/termApplications", async (req, res) => {
   ref
     .orderByChild("term")
-    .equalTo("Fall 2023")
-
-    .once("value", function fn(snapshot) {
+    .equalTo("Fall 2023") // Fetch all applications for <term> (e.g. Fall 2023)
+    // eslint-disable-next-line func-names
+    .once("value", function (snapshot) {
       const applications: Application[] = [];
       snapshot.forEach((childSnapshot) => {
         applications.push(childSnapshot.val());

From 9c0cb4c69269669e0345d93df5d60ace2f709646 Mon Sep 17 00:00:00 2001
From: HeetShah <heetshah7642@gmail.com>
Date: Sun, 19 Nov 2023 21:49:57 -0700
Subject: [PATCH 3/3] adds users.json file

---
 .gitignore                                    |  1 -
 .../typescript/graphql/sampleData/users.json  | 24 +++++++++++++++++++
 2 files changed, 24 insertions(+), 1 deletion(-)
 create mode 100644 backend/typescript/graphql/sampleData/users.json

diff --git a/.gitignore b/.gitignore
index 56e1dce..fab8049 100644
--- a/.gitignore
+++ b/.gitignore
@@ -13,4 +13,3 @@ backend/typescript/serviceAccount.json
 package-lock.json
 .gitignore
 package.json
-backend/typescript/graphql/sampleData/users.json
diff --git a/backend/typescript/graphql/sampleData/users.json b/backend/typescript/graphql/sampleData/users.json
new file mode 100644
index 0000000..86672be
--- /dev/null
+++ b/backend/typescript/graphql/sampleData/users.json
@@ -0,0 +1,24 @@
+[
+  {
+    "uid": "12345",
+    "email": "test@uwblueprint.org",
+    "emailVerified": true,
+    "displayName": "First and last",
+    "photoURL": "some link",
+    "disabled": false,
+    "metadata": {
+      "lastSignInTime": "last sign in time",
+      "creationTime": "creation time"
+    },
+    "tokensValidAfterTime": "tokens valid after time",
+    "providerData": [
+      {
+        "uid": "12345",
+        "displayName": "First and last",
+        "email": "test@uwblueprint.org",
+        "photoURL": "some link",
+        "providerId": "google.com"
+      }
+    ]
+  }
+]