From 75e7f5c073237dd03a9b66932a1ee126c81cebbc Mon Sep 17 00:00:00 2001 From: HeetShah 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 => { + 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 = {}; + 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 = {}; + // (Object.keys(teamToMembers)).forEach((team) => { + // teamToMemberSize[team] = teamToMembers[team].length; + // } + // ) + + const firebaseUsers: Record = {}; + 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 { + 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, +

+ 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. +

+ Sign into internal recruitment tool`; + + 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; + + /** + * 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; } export default IAuthService; From ecf6f6c4f8910766d963617ad61e59d641b7c2a2 Mon Sep 17 00:00:00 2001 From: HeetShah 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 = {}; 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 = {}; - // (Object.keys(teamToMembers)).forEach((team) => { - // teamToMemberSize[team] = teamToMembers[team].length; - // } - // ) - const firebaseUsers: Record = {}; 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 (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 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" + } + ] + } +]