Skip to content

Commit

Permalink
Merge branch 'main' into feature/andrewzpu/program-profile-pages
Browse files Browse the repository at this point in the history
  • Loading branch information
adhi0331 authored Jun 15, 2024
2 parents 6e45c21 + 6e67ad3 commit bce0f03
Show file tree
Hide file tree
Showing 26 changed files with 735 additions and 76 deletions.
2 changes: 2 additions & 0 deletions .github/workflows/build-and-deploy.yml
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,8 @@ jobs:
echo SERVICE_ACCOUNT_KEY=${{ secrets.SERVICE_ACCOUNT_KEY }} >> .env
echo APP_PORT=${{ secrets.APP_PORT }} >> .env
echo APP_FIREBASE_CONFIG=${{ secrets.APP_FIREBASE_CONFIG }} >> .env
echo EMAIL_ADDRESS_1=${{ secrets.EMAIL_ADDRESS_1 }} >> .env
echo PASS_1=${{ secrets.PASS_1 }} >> .env
- name: Build Frontend
run: cd frontend && npm ci && npm run build
- name: Build Backend
Expand Down
19 changes: 19 additions & 0 deletions backend/package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 2 additions & 0 deletions backend/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -30,11 +30,13 @@
"firebase-functions": "^4.7.0",
"mongodb": "^6.3.0",
"mongoose": "^8.3.1",
"nodemailer": "^6.9.13",
"tsc-alias": "^1.8.8"
},
"devDependencies": {
"@types/cors": "^2.8.17",
"@types/express": "^4.17.21",
"@types/nodemailer": "^6.4.14",
"@typescript-eslint/eslint-plugin": "^6.16.0",
"@typescript-eslint/parser": "^6.16.0",
"eslint": "^8.56.0",
Expand Down
55 changes: 50 additions & 5 deletions backend/src/controllers/program.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,11 @@ export type Program = {
color: string; //colorValueHex;
hourlyPay: string;
sessions: [string[]];
archived: boolean;
};

export type ExistingProgram = Program & {
dateUpdated: string;
};

export const createProgram: RequestHandler = async (req, res, next) => {
Expand All @@ -25,7 +30,10 @@ export const createProgram: RequestHandler = async (req, res, next) => {
try {
validationErrorParser(errors);

const programForm = await ProgramModel.create(req.body as Program);
const programForm = await ProgramModel.create({
...(req.body as Program),
dateUpdated: new Date().toISOString(),
});

res.status(201).json(programForm);
} catch (error) {
Expand All @@ -39,16 +47,27 @@ export const updateProgram: RequestHandler = async (req, res, next) => {
validationErrorParser(errors);

const programId = req.params.id;
const programData = req.body as Program;
const programData = req.body as ExistingProgram;

const editedProgram = await ProgramModel.findOneAndUpdate({ _id: programId }, programData, {
new: true,
});
const editedProgram = await ProgramModel.findOneAndUpdate(
{ _id: programId },
{ ...programData, archived: false, dateUpdated: new Date().toISOString() }, //stand-in method of un-archiving programs
{
new: true,
},
);

if (!editedProgram) {
return res.status(404).json({ message: "No object in database with provided ID" });
}

// Waitlist all archived students. Making sure to only waitlist Archived students
// will prevent enrollments from being updated every time the program is updated
await EnrollmentModel.updateMany(
{ programId: { $eq: programId }, status: { $eq: "Archived" } },
{ $set: { status: "Waitlisted", dateUpdated: Date.now() } },
);

res.status(200).json(editedProgram);
} catch (error) {
next(error);
Expand All @@ -66,6 +85,32 @@ export const getProgram: RequestHandler = async (req, res, next) => {
}

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

export const archiveProgram: RequestHandler = async (req, res, next) => {
const errors = validationResult(req);
try {
validationErrorParser(errors);

const programId = req.params.id;
const program = await ProgramModel.findByIdAndUpdate(
programId,
{ $set: { archived: true, dateUpdated: new Date().toISOString() } },
{ new: true },
);
if (!program)
return res.status(404).json({ message: "Program with this id not found in database" });

//Archive all students
await EnrollmentModel.updateMany(
{ programId: { $eq: programId } },
{ $set: { status: "Archived", dateUpdated: Date.now() } },
);

return res.status(200).json(program);
} catch (error) {
next(error);
}
Expand Down
94 changes: 91 additions & 3 deletions backend/src/controllers/user.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,8 +8,10 @@ import { ServiceError } from "../errors/service";
import { ValidationError } from "../errors/validation";
import { Image } from "../models/image";
import UserModel from "../models/user";
import { sendApprovalEmail, sendDenialEmail } from "../util/email";
import { firebaseAdminAuth } from "../util/firebase";
import { handleImageParsing } from "../util/image";
import { deleteUserFromFirebase, deleteUserFromMongoDB } from "../util/user";
import validationErrorParser from "../util/validationErrorParser";

import { UserIdRequestBody } from "./types/types";
Expand Down Expand Up @@ -49,9 +51,6 @@ export const createUser = async (
name,
accountType,
email,
// profilePicture default "default" in User constructor
// lastChangedPassword default Date.now() in User constructor
// approvalStatus default false in User constructor
});

res.status(201).json(newUser);
Expand All @@ -63,6 +62,95 @@ export const createUser = async (
return;
};

export const deleteUser = async (req: Request, res: Response, nxt: NextFunction) => {
try {
const { email } = req.params;

// Find the user by email
const user = await UserModel.findOne({ email });
if (!user) {
throw new Error("User not found");
}

const userId = user._id; // _id is the uid in schema

// delete user from Firebase and MongoDB
await deleteUserFromFirebase(userId);
await deleteUserFromMongoDB(userId);

res.status(200).send("User deleted successfully");
} catch (error) {
console.error("Error deleting user:", error);
nxt(error);
}
};

export const getNotApprovedUsers = async (req: Request, res: Response, next: NextFunction) => {
try {
// const notApprovedUsers: User[] = await UserModel.find({ approvalStatus: false }).exec();
const notApprovedUsers = await UserModel.find({ approvalStatus: false }).exec();

res.status(200).json(notApprovedUsers);
} catch (error) {
console.error("Error fetching not-approved users:", error);
next(error);
}
};

export const approveUser = async (req: Request, res: Response, nxt: NextFunction) => {
try {
const { email } = req.body;

Check warning on line 102 in backend/src/controllers/user.ts

View workflow job for this annotation

GitHub Actions / Backend lint and style check

Unsafe assignment of an `any` value

const user = await UserModel.findOne({ email });

Check warning on line 104 in backend/src/controllers/user.ts

View workflow job for this annotation

GitHub Actions / Backend lint and style check

Unsafe assignment of an `any` value
if (!user) {
return res.status(404).send("User not found");
}

const userId = user._id;

await UserModel.findByIdAndUpdate(userId, { approvalStatus: true });

// await sendApprovalEmail(email);
await sendApprovalEmail(email as string);

res.status(200).send("User approved successfully");
} catch (error) {
console.error(error);
nxt(error);
}
};

export const denyUser = async (req: Request, res: Response, nxt: NextFunction) => {
console.log("Inside denyUser controller");

try {
const { email } = req.body;

Check warning on line 127 in backend/src/controllers/user.ts

View workflow job for this annotation

GitHub Actions / Backend lint and style check

Unsafe assignment of an `any` value

console.log("Email from request body:", email);

// const user = await UserModel.findOne({ email });
const user = await UserModel.findOne({ email });

Check warning on line 132 in backend/src/controllers/user.ts

View workflow job for this annotation

GitHub Actions / Backend lint and style check

Unsafe assignment of an `any` value

if (!user) {
return res.status(404).send("User not found");
}

console.log("User object:", user);

const userId = user._id;

await UserModel.findByIdAndUpdate(userId, { approvalStatus: false });

console.log(email as string);
await sendDenialEmail(email as string);

res.status(200).send("User denied successfully");
} catch (error) {
console.error(error);
nxt(error);
}
};

export const loginUser = async (
req: Request<Record<string, never>, Record<string, never>, LoginUserRequestBody>,
res: Response,
Expand Down
3 changes: 3 additions & 0 deletions backend/src/models/program.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,9 @@ const programSchema = new Schema({
color: { type: String, required: true },
hourlyPay: { type: Number, required: true },
sessions: { type: [[String]], required: true },
archived: { type: Boolean, required: true },

dateUpdated: { type: String, required: true },
});

type Program = InferSchemaType<typeof programSchema>;
Expand Down
1 change: 1 addition & 0 deletions backend/src/routes/program.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ const router = express.Router();

router.patch("/:id", ProgramValidator.updateProgram, ProgramController.updateProgram);
router.post("/create", ProgramValidator.createProgram, ProgramController.createProgram);
router.post("/archive/:id", ProgramController.archiveProgram);

Check failure on line 14 in backend/src/routes/program.ts

View workflow job for this annotation

GitHub Actions / Backend lint and style check

Unsafe argument of type `any` assigned to a parameter of type `RequestHandler<{ id: string; }, any, any, ParsedQs, Record<string, any>>`
router.get("/all", ProgramController.getAllPrograms);

Check failure on line 15 in backend/src/routes/program.ts

View workflow job for this annotation

GitHub Actions / Backend lint and style check

Unsafe argument of type `any` assigned to a parameter of type `RequestHandler<{}, any, any, ParsedQs, Record<string, any>>`
router.get("/:id", ProgramController.getProgram);
router.get("/enrollments/:id", ProgramController.getProgramEnrollments);

Check failure on line 17 in backend/src/routes/program.ts

View workflow job for this annotation

GitHub Actions / Backend lint and style check

Unsafe argument of type `any` assigned to a parameter of type `RequestHandler<{ id: string; }, any, any, ParsedQs, Record<string, any>>`
Expand Down
9 changes: 9 additions & 0 deletions backend/src/routes/user.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,15 @@ const router = express.Router();
router.use(express.json());

router.post("/create", UserValidator.createUser, UserController.createUser);

Check warning on line 11 in backend/src/routes/user.ts

View workflow job for this annotation

GitHub Actions / Backend lint and style check

Promise returned in function argument where a void return was expected

router.post("/approve", [verifyAuthToken], UserController.approveUser);

Check warning on line 13 in backend/src/routes/user.ts

View workflow job for this annotation

GitHub Actions / Backend lint and style check

Promise returned in function argument where a void return was expected

router.post("/deny", [verifyAuthToken], UserController.denyUser);

router.delete("/delete/:email", [verifyAuthToken], UserController.deleteUser);

router.get("/not-approved", [verifyAuthToken], UserController.getNotApprovedUsers);

router.get("/", [verifyAuthToken], UserController.loginUser);
router.post("/editPhoto", [verifyAuthToken], UserValidator.editPhoto, UserController.editPhoto);
router.get("/getPhoto/:id", [verifyAuthToken], UserController.getPhoto);
Expand Down
59 changes: 59 additions & 0 deletions backend/src/util/email.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
import dotenv from "dotenv";
dotenv.config();

import nodemailer from "nodemailer";

// Create a transporter object using SMTP transport
const transporter = nodemailer.createTransport({
service: "Gmail",
auth: {
user: process.env.EMAIL_ADDRESS_1,
pass: process.env.PASS_1,
},
});

export const sendApprovalEmail = async (email: string) => {
try {
await transporter.sendMail({
from: process.env.EMAIL_ADDRESS_1,
to: email,
subject: "Welcome to PIA! Your Account Has Been Approved",
// text: `Hello,
// Thank you for your interest in Plant It Again.
// We are emailing to let you know that your account
// creation request has been approved.`
html: `<p>Hello,</p>
<p>Thank you for your interest in Plant It Again.</p>
<p>We are emailing to let you know that your account creation request
has been <strong>approved</strong>.</p>`,
});
console.log("Approval email sent successfully");
} catch (error) {
console.error("Error sending approval email:", error);
}
};

export const sendDenialEmail = async (email: string) => {
console.log("Sending Denial Email");
try {
await transporter.sendMail({
from: process.env.EMAIL_ADDRESS_1,
to: email,
subject: "An Update on Your PIA Account Approval Status",
// text: `Hello,
// Thank you for your interest in Plant It Again.
// We are emailing to let you know that your account
// creation request has been denied.
// If you believe this a mistake,
// please contact us through our website`
html: `<p>Hello,</p>
<p>Thank you for your interest in Plant It Again.</p>
<p>We are emailing to let you know that your account creation request
has been <strong>denied</strong>.</p>
<p>If you believe this is a mistake, please contact us through our website.</p>`,
});
console.log("Denial email sent successfully");
} catch (error) {
console.error("Error sending denial email:", error);
}
};
13 changes: 13 additions & 0 deletions backend/src/util/user.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
import UserModel from "../models/user";

import { firebaseAdminAuth } from "./firebase";

// delete user from Firebase
export const deleteUserFromFirebase = async (userId: string): Promise<void> => {
await firebaseAdminAuth.deleteUser(userId);
};

// delete user from MongoDB
export const deleteUserFromMongoDB = async (userId: string): Promise<void> => {
await UserModel.findByIdAndDelete(userId);
};
Loading

0 comments on commit bce0f03

Please sign in to comment.