Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[INTF23] Non Authenticated BP members #42

Merged
merged 3 commits into from
Nov 20, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions backend/typescript/graphql/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -89,6 +89,7 @@ const graphQLMiddlewares = {
deleteUserByEmail: authorizedByAdmin(),
logout: isAuthorizedByUserId("userId"),
resetPassword: isAuthorizedByEmail("email"),
sendSignInLink: authorizedByAllRoles(),
},
};

Expand Down
15 changes: 13 additions & 2 deletions backend/typescript/graphql/resolvers/authResolvers.ts
Original file line number Diff line number Diff line change
@@ -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";
Expand All @@ -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();

Expand Down Expand Up @@ -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;
},
},
};

Expand Down
24 changes: 24 additions & 0 deletions backend/typescript/graphql/sampleData/users.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
[
{
"uid": "12345",
"email": "[email protected]",
"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": "[email protected]",
"photoURL": "some link",
"providerId": "google.com"
}
]
}
]
1 change: 1 addition & 0 deletions backend/typescript/graphql/types/authType.ts
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,7 @@ const authType = gql`
refresh(refreshToken: String!): String!
logout(userId: ID!): ID
resetPassword(email: String!): Boolean!
sendSignInLink(email: String!): Boolean!
}
`;

Expand Down
57 changes: 53 additions & 4 deletions backend/typescript/server.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down Expand Up @@ -63,12 +65,59 @@ admin.initializeApp({
const db = admin.database();
const ref = db.ref("studentApplications");

app.get("/diff", async (req, res) => {
const currentTerm = memeberData.term;
const currentTermMembers: string[] = [];

memeberData.members.forEach((member) => {
if (member.term === currentTerm) {
currentTermMembers.push(member.name);
}
});

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")
.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());
Expand Down Expand Up @@ -103,13 +152,13 @@ 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.");
}
});

app.listen({ port: process.env.PORT || 5000 }, () => {
// eslint-disable-next-line no-console
console.info(`Server is listening on port ${process.env.PORT || 5000}!`);
});
49 changes: 49 additions & 0 deletions backend/typescript/services/implementations/authService.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
8 changes: 8 additions & 0 deletions backend/typescript/services/interfaces/authService.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Loading