Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
41 commits
Select commit Hold shift + click to select a range
29c902c
set up sendgrid
Apr 3, 2025
b4e17f9
Implement user type frontend page
jkim-21 Apr 7, 2025
8d454e6
Format table to be similar to all users page
jkim-21 Apr 7, 2025
483152f
existing meetings work
theannachen Apr 14, 2025
4eb4707
new meetings working
theannachen Apr 14, 2025
5569eae
webinars also work noww
theannachen Apr 15, 2025
15fa7fd
Merge branch 'main' into anna/zoom
theannachen Apr 19, 2025
4b545fa
checkpoint
kohrachel Apr 20, 2025
8073168
Retrieve all info from backend + display certification on users table…
kohrachel Apr 20, 2025
f1051df
finish linking stuff and removing unnecessary course stuff
theannachen Apr 21, 2025
803b70c
changed some frontend stuff
theannachen Apr 21, 2025
2e40b38
added reset password design
ashrit-ram-anala Apr 21, 2025
00c5ce9
added images folder
ashrit-ram-anala Apr 21, 2025
dcb694f
git was weird and nono let me push but its okay now
ashrit-ram-anala Apr 21, 2025
95d23ff
Create user type model and controller
jkim-21 Apr 21, 2025
8b748d3
Connect the usertypes dynamic roles with user types page
jkim-21 Apr 21, 2025
508274c
Provide delete and update functionality
jkim-21 Apr 21, 2025
b154958
email with placeholders working
Apr 23, 2025
3dc80c8
templates working
Apr 23, 2025
5fe6c8d
added user type with request sent to backend with auth service
ashrit-ram-anala Apr 24, 2025
8fe2cef
finished changes
theannachen Apr 28, 2025
ac4f7db
email scheduling
May 4, 2025
27b92a7
email done
May 6, 2025
b1a5a11
minor
May 6, 2025
7774729
Merge pull request #110 from ChangePlusPlusVandy/yifei-email
yifeifang11 May 6, 2025
82e1814
Merge branch 'main' into anna/zoom
yifeifang11 May 6, 2025
ac55fa6
Merge pull request #106 from ChangePlusPlusVandy/anna/zoom
yifeifang11 May 6, 2025
dff7686
Merge branch 'main' into user-type-page
yifeifang11 May 6, 2025
b486c14
merge main
May 6, 2025
65fc382
added prices to user types
May 6, 2025
90cec05
pricing reflects on user side
May 7, 2025
e177803
merge conflict
May 7, 2025
c007de9
Merge pull request #91 from ChangePlusPlusVandy/user-type-page
yifeifang11 May 7, 2025
0201fed
delete package-lock outside
May 7, 2025
7e867ac
Merge branch 'main' into rachel/admin-user-certification
yifeifang11 May 7, 2025
6cdae92
Merge pull request #105 from ChangePlusPlusVandy/rachel/admin-user-ce…
yifeifang11 May 7, 2025
b0d6eb5
resolve conflicts
May 7, 2025
842bdf7
paths
May 7, 2025
3ea6aec
fix bugs
May 7, 2025
0bcbd78
reset password
May 7, 2025
83fd244
user dashboard fixes
May 7, 2025
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
21 changes: 9 additions & 12 deletions backend/app.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,10 @@ import handoutRoutes from "./routes/handoutRoutes";
import courseCategoriesRoutes from "./routes/courseCategoryRoutes";
import emailRoutes from "./routes/emailRoutes";
import speakerRoutes from "./routes/speakerRoutes";
import pdfRoutes from "./routes/pdfRoutes";
import pdfRoutes from "./routes/pdfRoutes";
import emailTemplateRoutes from "./routes/emailTemplateRoutes";
import zoomRoutes from "./routes/zoomRoutes";
import userTypeRoutes from "./routes/userTypeRoutes";

// Import middleware
import { notFound, errorHandler } from "./middlewares/errorMiddleware";
Expand All @@ -30,6 +33,7 @@ import upload from "./middlewares/upload";
import { uploadImage } from "./controllers/uploadController";
import uploadRoutes from "./routes/uploadRoutes";
import dotenv from "dotenv";

dotenv.config();

const app: Application = express();
Expand Down Expand Up @@ -88,20 +92,13 @@ app.use("/api/certificates", verifyFirebaseAuth, certificateRoutes);
app.use("/api/courseCategories", verifyFirebaseAuth, courseCategoriesRoutes);
app.use("/api/handout", verifyFirebaseAuth, handoutRoutes);
app.use("/api/emails", verifyFirebaseAuth, emailRoutes);
app.use("/api/emailTemplates", verifyFirebaseAuth, emailTemplateRoutes);
app.use("/api/speakers", verifyFirebaseAuth, speakerRoutes);
app.use("/api/upload", verifyFirebaseAuth, uploadRoutes);
app.use("/api/certificatePDFs", verifyFirebaseAuth, pdfRoutes);
app.use("/api/user-types", userTypeRoutes);
app.use("/api/zoom", verifyFirebaseAuth, zoomRoutes);
app.use("/api/certificatePDFs", verifyFirebaseAuth, pdfRoutes);

// Error middleware
app.use((err: any, req: Request, res: Response, next: NextFunction): void => {
if (err instanceof multer.MulterError) {
console.error("❌ Multer error:", err.message);
res.status(400).json({ message: err.message });
return;
}
console.error("❌ Unknown error:", err);
res.status(500).json({ message: "Internal server error" });
});
app.use(notFound);
app.use(errorHandler);

Expand Down
2 changes: 0 additions & 2 deletions backend/config/cloudinaryStorage.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,6 @@ import cloudinary from "./cloudinary";
import { Request } from "express";
import { Options } from "multer-storage-cloudinary";

console.log("cloundaryStorage");

// Explicit type to override the broken one
interface CustomParams {
folder: string;
Expand Down
34 changes: 34 additions & 0 deletions backend/config/resend.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
import dotenv from "dotenv";
dotenv.config();

import { Resend } from "resend";
import handlebars from "handlebars";

const resend = new Resend(process.env.RESEND_API_KEY!);

export const sendEmail = async (
to: string | string[],
subject: string,
body: string, // Handlebars HTML string
variables: Record<string, any>
) => {
const template = handlebars.compile(body);
const html = template(variables);

try {
const { data, error } = await resend.emails.send({
from: "Foster Source <[email protected]>",
to,
subject,
html,
});

if (error) {
console.error("Email send error:", error);
throw new Error("Failed to send email");
}
} catch (err) {
console.error(err);
throw err;
}
};
28 changes: 5 additions & 23 deletions backend/controllers/courseController.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,12 +24,9 @@ export const getCourses = async (
): Promise<void> => {
try {
const filters = req.query;

// Populate ratings and components fields as needed
const courseResponses = await Course.find(filters)
.populate(["ratings", "components"])
.populate(["ratings"])
.exec();

res.status(200).json({
success: true,
count: courseResponses.length,
Expand All @@ -56,7 +53,7 @@ export const getCourseById = async (
if (id) {
// Find course by ID and populate related fields
const course = await Course.findById(id)
.populate(["ratings", "components", "managers"])
.populate(["ratings", "managers"])
.exec();

if (!course) {
Expand Down Expand Up @@ -105,8 +102,6 @@ export const createCourse = async (
ratings,
className,
discussion,
components,
isLive,
categories,
creditNumber,
courseDescription,
Expand All @@ -115,17 +110,14 @@ export const createCourse = async (
cost,
instructorDescription,
instructorRole,
lengthCourse,
time,
instructorName,
isInPerson,
students,
managers,
speakers,
courseType,
regStart,
regEnd,
productType,
productInfo,
shortUrl,
draft,
} = req.body;
Expand All @@ -134,23 +126,18 @@ export const createCourse = async (
if (!draft)
if (
!className ||
isLive === undefined ||
creditNumber === undefined ||
!courseDescription ||
!thumbnailPath ||
cost === undefined ||
!lengthCourse ||
!time ||
!instructorName ||
isInPerson === undefined ||
!courseType ||
!regEnd
) {
// console.log("[createCourse] Validation failed. Missing required fields");
res.status(400).json({
success: false,
message:
"Please provide className, isLive, creditNumber, thumbnailPath, cost, lengthCourse, time, instructorName, isInPerson, courseType, and regStart",
"Please provide className, isLive, creditNumber, thumbnailPath, cost, lengthCourse, instructorName, and regStart",
});
return;
}
Expand All @@ -172,8 +159,6 @@ export const createCourse = async (
ratings,
className,
discussion,
components,
isLive,
categories,
creditNumber,
courseDescription,
Expand All @@ -182,17 +167,14 @@ export const createCourse = async (
cost,
instructorDescription,
instructorRole,
lengthCourse,
time,
instructorName,
isInPerson,
students,
managers,
speakers,
courseType,
regStart,
regEnd,
productType,
productInfo,
shortUrl,
draft,
});
Expand Down
163 changes: 163 additions & 0 deletions backend/controllers/emailController.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,163 @@
import { Request, Response } from "express";
import Progress, { IProgress } from "../models/progressModel";
import Email, { IEmail } from "../models/emailModel";
import User from "../models/userModel";
import Course from "../models/courseModel";
import { sendEmail } from "../config/resend";
import { emailQueue } from "../jobs/emailQueue";
import mongoose from "mongoose";

// @desc Get all emails or filter by query parameters
// @route GET /api/email
// @access Public
export const getEmails = async (req: Request, res: Response): Promise<void> => {
try {
const emails = await Email.find(req.query)
.populate({
path: "course",
select: "className",
})
.sort({ sendDate: -1 });
res.status(200).json(emails);
} catch (error) {
res.status(500).json({ message: "Error fetching progress data", error });
}
};

// @desc Create a new email
// @route POST /api/email
// @access Public
export const createEmail = async (
req: Request,
res: Response
): Promise<void> => {
try {
const newEmail: IEmail = new Email(req.body);
await newEmail.save();
res.status(201).json(newEmail);
} catch (error) {
res.status(400).json({ message: "Error creating progress entry", error });
}
};

// @desc Create and send a new email
// @route POST /api/email/send
// @access Public
export const createAndSendEmail = async (
req: Request,
res: Response
): Promise<void> => {
try {
const { subject, body, courseId, sendDate } = req.body;

const newEmail: IEmail = new Email({
course: courseId,
subject,
body,
sendDate: sendDate ? new Date(sendDate) : new Date(),
sent: false,
});
await newEmail.save();

const emailId = (newEmail._id as mongoose.Types.ObjectId).toString();

const delay = sendDate
? Math.max(new Date(sendDate).getTime() - Date.now(), 0)
: 0;

await emailQueue.add(
"send-course-email", // match worker's processor name too
{ emailId },
{
delay,
jobId: emailId, // this ensures you can later retrieve/remove it
}
);

console.log("added to queue");

res.status(200).json(newEmail);
} catch (error) {
console.error(error);
res
.status(400)
.json({ message: "Error creating and queuing email", error });
}
};

// @desc Update an email
// @route PUT /api/email/:id
// @access Public
export const updateEmail = async (
req: Request,
res: Response
): Promise<void> => {
console.log(req.params);
const { id } = req.params; // Get the ID from the request parameters
const { subject, body, courseId, sendDate } = req.body;
try {
const updatedEmail = await Email.findByIdAndUpdate(
id,
{
subject,
body,
course: courseId,
sendDate,
wasSent: false,
},
{ new: true }
);

if (!updatedEmail) {
res.status(404).json({ message: "Email not found" });
}

// Remove old job
const existingJob = await emailQueue.getJob(id);
if (existingJob) {
await existingJob.remove();
}

// Re-schedule job
const delay = new Date(sendDate).getTime() - Date.now();

await emailQueue.add(
"send-course-email",
{ emailId: id },
{
jobId: id,
delay: Math.max(delay, 0),
}
);

res.status(200).json(updatedEmail);
} catch (error) {
console.error(error);
res.status(500);
}
};

// @desc Delete an email
// @route DELETE /api/email/:id
// @access Public
export const deleteEmail = async (
req: Request,
res: Response
): Promise<void> => {
const { id } = req.params; // Get the ID from the request parameters
try {
const deletedEmail = await Email.findByIdAndDelete(id);
if (!deletedEmail) {
res.status(404).json({ message: "Email not found" });
}

const existingJob = await emailQueue.getJob(id);
if (existingJob) {
await existingJob.remove();
}

res.status(204).send(); // Send a 204 No Content response
} catch (error) {
res.status(500);
}
};
Loading