Skip to content

Commit

Permalink
Notification emails to user & admin when password is changed
Browse files Browse the repository at this point in the history
  • Loading branch information
benjaminJohnson2204 committed May 28, 2024
1 parent 357d6bf commit 6c8e6d8
Show file tree
Hide file tree
Showing 5 changed files with 94 additions and 28 deletions.
22 changes: 22 additions & 0 deletions backend/src/controllers/user.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,10 @@ import UserModel, { DisplayUser, UserRole } from "src/models/user";
import { validationResult } from "express-validator";
import validationErrorParser from "src/util/validationErrorParser";
import createHttpError from "http-errors";
import {
sendOwnPasswordChangedNotificationEmail,
sendPasswordChangedEmailToAdmin,
} from "src/services/emails";

/**
* Retrieves data about the current user (their MongoDB ID, Firebase UID, and role).
Expand Down Expand Up @@ -99,12 +103,30 @@ export const changeUserPassword: RequestHandler = async (req, res, next) => {
password,
});

await sendPasswordChangedEmailToAdmin(updatedUser.email!);

res.status(200).json(updatedUser);
} catch (error) {
next(error);
}
};

/**
* Sends an email to notify the user that their password has been reset.
*/
export const notifyResetPassword: RequestHandler = async (req: PAPRequest, res, next) => {
try {
const { userUid } = req;
const firebaseUser = await firebaseAuth.getUser(userUid!);
await sendOwnPasswordChangedNotificationEmail(firebaseUser.email!);
await sendPasswordChangedEmailToAdmin(firebaseUser.email!);

res.status(204).send();
} catch (error) {
next(error);
}
};

/**
* Deletes a user from the Firebase and MongoDB databases
*/
Expand Down
8 changes: 7 additions & 1 deletion backend/src/routes/user.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import express from "express";

import { requireAdmin, requireSignedIn } from "src/middleware/auth";
import { requireAdmin, requireSignedIn, requireStaffOrAdmin } from "src/middleware/auth";
import * as UserController from "src/controllers/user";
import * as UserValidator from "src/validators/user";

Expand All @@ -22,6 +22,12 @@ router.patch(
UserValidator.changeUserPassword,
UserController.changeUserPassword,
);
router.post(
"/notifyResetPassword",
requireSignedIn,
requireStaffOrAdmin,
UserController.notifyResetPassword,
);
router.delete("/:uid", requireSignedIn, requireAdmin, UserController.deleteUser);

export default router;
75 changes: 49 additions & 26 deletions backend/src/services/emails.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import "dotenv/config";
import nodemailer from "nodemailer";
import Mail from "nodemailer/lib/mailer";
import env from "src/util/validateEnv";

const trimmedFrontendUrl = env.FRONTEND_ORIGIN.replace(
Expand All @@ -8,6 +9,23 @@ const trimmedFrontendUrl = env.FRONTEND_ORIGIN.replace(
"",
);

const sendEmail = async (options: Mail.Options) => {
const transporter = nodemailer.createTransport({
service: "gmail",
auth: {
user: env.EMAIL_USER,
pass: env.EMAIL_APP_PASSWORD,
},
});

const mailOptions = {
from: env.EMAIL_USER,
...options,
};

await transporter.sendMail(mailOptions);
};

/**
* Sends a notification email to PAP staff when a VSR is submitted.
* Throws an error if the email could not be sent.
Expand All @@ -20,22 +38,11 @@ const sendVSRNotificationEmailToStaff = async (name: string, email: string, id:
const EMAIL_SUBJECT = "New VSR Submitted";
const EMAIL_BODY = `A new VSR was submitted by ${name} from ${email}. You can view it at ${trimmedFrontendUrl}/staff/vsr/${id}`;

const transporter = nodemailer.createTransport({
service: "gmail",
auth: {
user: env.EMAIL_USER,
pass: env.EMAIL_APP_PASSWORD,
},
});

const mailOptions = {
from: env.EMAIL_USER,
await sendEmail({
to: env.EMAIL_NOTIFICATIONS_RECIPIENT,
subject: EMAIL_SUBJECT,
text: EMAIL_BODY,
};

await transporter.sendMail(mailOptions);
});
};

/**
Expand Down Expand Up @@ -112,16 +119,7 @@ const sendVSRConfirmationEmailToVeteran = async (name: string, email: string) =>
<p>Instagram patriotsandpaws</p>\
`;

const transporter = nodemailer.createTransport({
service: "gmail",
auth: {
user: env.EMAIL_USER,
pass: env.EMAIL_APP_PASSWORD,
},
});

const mailOptions = {
from: env.EMAIL_USER,
await sendEmail({
to: email,
subject: EMAIL_SUBJECT,
html: EMAIL_HTML,
Expand All @@ -132,9 +130,34 @@ const sendVSRConfirmationEmailToVeteran = async (name: string, email: string) =>
cid: "pap_logo.png",
},
],
};
});
};

await transporter.sendMail(mailOptions);
const sendOwnPasswordChangedNotificationEmail = async (email: string) => {
const EMAIL_SUBJECT = "PAP Application Password Change Confirmation";
const EMAIL_BODY = `Your password for the ${email} account has been changed.`;

await sendEmail({
to: email,
subject: EMAIL_SUBJECT,
text: EMAIL_BODY,
});
};

const sendPasswordChangedEmailToAdmin = async (email: string) => {
const EMAIL_SUBJECT = "PAP Application Password Change Notification";
const EMAIL_BODY = `The password for the ${email} account has been changed.`;

await sendEmail({
to: env.EMAIL_NOTIFICATIONS_RECIPIENT,
subject: EMAIL_SUBJECT,
text: EMAIL_BODY,
});
};

export { sendVSRNotificationEmailToStaff, sendVSRConfirmationEmailToVeteran };
export {
sendVSRNotificationEmailToStaff,
sendVSRConfirmationEmailToVeteran,
sendOwnPasswordChangedNotificationEmail,
sendPasswordChangedEmailToAdmin,
};
9 changes: 9 additions & 0 deletions frontend/src/api/Users.ts
Original file line number Diff line number Diff line change
Expand Up @@ -62,6 +62,15 @@ export const changeUserPassword = async (
}
};

export const notifyResetPassword = async (firebaseToken: string): Promise<APIResult<null>> => {
try {
await post("/api/user/notifyResetPassword", {}, createAuthHeader(firebaseToken));
return { success: true, data: null };
} catch (error) {
return handleAPIError(error);
}
};

export const deleteUser = async (uid: string, firebaseToken: string): Promise<APIResult<null>> => {
try {
await httpDelete(`/api/user/${uid}`, createAuthHeader(firebaseToken));
Expand Down
8 changes: 7 additions & 1 deletion frontend/src/app/handlePasswordReset/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ import { FirebaseError } from "firebase/app";
import { NotificationBanner } from "@/components/shared/NotificationBanner";
import { LoadingScreen } from "@/components/shared/LoadingScreen";
import styles from "@/app/handlePasswordReset/page.module.css";
import { notifyResetPassword } from "@/api/Users";

enum ResetPasswordPageError {
NO_INTERNET,
Expand Down Expand Up @@ -123,7 +124,12 @@ const PasswordReset: React.FC = () => {
}

try {
await signInWithEmailAndPassword(auth, email, data.newPassword);
const { user } = await signInWithEmailAndPassword(auth, email, data.newPassword);
const firebaseToken = await user.getIdToken();
const result = await notifyResetPassword(firebaseToken);
if (!result.success) {
console.error(`Notifying user of password reset failed with error ${result.error}`);
}
} catch (error) {
console.error("Firebase login failed with error: ", error);

Expand Down

0 comments on commit 6c8e6d8

Please sign in to comment.