From c314da8c3fc918c25057474e2a5cac2ca3062dce Mon Sep 17 00:00:00 2001 From: Heryan Djaruma Date: Fri, 18 Jul 2025 09:31:48 +0700 Subject: [PATCH 01/40] init mentoring system --- .../src/controllers/mentorship_controller.ts | 77 +++++++++++++++++++ functions/src/data/dummy/mentorship.ts | 28 +++++++ functions/src/data/dummy/user.ts | 32 ++++++++ functions/src/models/mentorship.ts | 20 +++++ functions/src/models/user.ts | 2 + functions/src/routes/index.ts | 2 + functions/src/server.ts | 2 + functions/src/utils/fake_data_populator.ts | 39 ++++++++++ 8 files changed, 202 insertions(+) create mode 100644 functions/src/controllers/mentorship_controller.ts create mode 100644 functions/src/data/dummy/mentorship.ts create mode 100644 functions/src/data/dummy/user.ts create mode 100644 functions/src/models/mentorship.ts diff --git a/functions/src/controllers/mentorship_controller.ts b/functions/src/controllers/mentorship_controller.ts new file mode 100644 index 0000000..5c40b85 --- /dev/null +++ b/functions/src/controllers/mentorship_controller.ts @@ -0,0 +1,77 @@ +import { db } from "../config/firebase" +import { FirestoreMentor, MentorshipAppointment } from "../models/mentorship"; +import { Request, Response } from "express"; + +export const getMentors = async ( + req: Request, + res: Response +): Promise => { + let allMentors: FirestoreMentor[] = []; + + try { + const snapshot = await db.collection('users') + .where("mentor", "==", true) + .get() + snapshot.docs.map((mentor) => { + allMentors.push({ + id: mentor.id, + ...mentor.data() + } as FirestoreMentor) + }) + res.status(200).json({ allMentors }) + } catch (error) { + res.status(500).json({ error: (error as Error).message }); + } +} + +export const getMentorshipAppointments = async ( + req: Request, + res: Response +): Promise => { + let mentorshipAppointments: MentorshipAppointment[] = []; + + try { + const snapshot = await db.collection('mentorships') + .where("mentor", "==", true) + .get() + snapshot.docs.map((mentor) => { + mentorshipAppointments.push({ + id: mentor.id, + ...mentor.data() + } as MentorshipAppointment) + }) + res.status(200).json({ mentorshipAppointments }) + } catch (error) { + res.status(500).json({ error: (error as Error).message }); + } +} + +export const getMentorshipAppointmentsByMentorId = async ( + req: Request, + res: Response +): Promise => { + try { + const { mentorId } = req.params; + const doc = await db.collection("mentorships") + .where("mentor", "==", true) + .where("mentorId", "==", mentorId) + .get(); + + if (!doc.empty) { + res.status(404).json({ error: "Cannot find mentorships related with the mentor." }); + return; + } + + let mentorships: MentorshipAppointment[] = []; + doc.docs.map((mentorship) => { + mentorships.push({ + id: mentorship.id, + ...mentorship.data() + } as MentorshipAppointment) + }) + + res.status(200).json(mentorships); + } catch (error) { + res.status(500).json({ error: (error as Error).message }); + } +}; \ No newline at end of file diff --git a/functions/src/data/dummy/mentorship.ts b/functions/src/data/dummy/mentorship.ts new file mode 100644 index 0000000..5758c88 --- /dev/null +++ b/functions/src/data/dummy/mentorship.ts @@ -0,0 +1,28 @@ +import { MentorshipAppointment } from "../../models/mentorship"; + +export const dummy_mentorships: MentorshipAppointment[] = [ + { + "mentorId": "-OVQFbrn-Ct6u6SSB47I", + "startTime": 1752804000, + "endTime": 1752804900, + "location": "online", + }, + { + "mentorId": "-OVQFbrn-Ct6u6SSB47I", + "startTime": 1752804900, + "endTime": 1752805800, + "location": "online", + }, + { + "mentorId": "-OVQFrFPecnWerr4SwXn", + "startTime": 1752629400, + "endTime": 1752628500, + "location": "offline", + }, + { + "mentorId": "-OVQFrFPecnWerr4SwXn", + "startTime": 1752629400, + "endTime": 1752630300, + "location": "offline", + }, +] \ No newline at end of file diff --git a/functions/src/data/dummy/user.ts b/functions/src/data/dummy/user.ts new file mode 100644 index 0000000..dc18b69 --- /dev/null +++ b/functions/src/data/dummy/user.ts @@ -0,0 +1,32 @@ +import { FirestoreMentor } from "../../models/mentorship"; +import { User } from "../../models/user"; + +export const dummy_mentors: FirestoreMentor[] = [ + { + "id": "-OVQFbrn-Ct6u6SSB47I", + mentor: true, + "email": "dummy@mentor.com", + "name": "Lorem Ipsum", + "specialization": "backend,frontend", + "discordUsername": "lole", + "intro": "Hello fwens" + }, + { + "id": "-OVQFrFPecnWerr4SwXn", + mentor: true, + "email": "dummy2@mentor.com", + "name": "John Pork", + "specialization": "product manager,designer", + "discordUsername": "hehei", + "intro": "Hi guuyss" + } +] + +export const dummy_hackers: User[] = [ + { + "id": "-OVQHbXAlQiM5NQHrTe1", + "email": "hacker@gmail.com", + "firstName": "Hacker", + "lastName": "Last", + } +] \ No newline at end of file diff --git a/functions/src/models/mentorship.ts b/functions/src/models/mentorship.ts new file mode 100644 index 0000000..6bda678 --- /dev/null +++ b/functions/src/models/mentorship.ts @@ -0,0 +1,20 @@ +export interface FirestoreMentor { + id?: string; + email: string; + name: string; + mentor: boolean; + specialization: string; + discordUsername: string; + intro: string; // introduction given by mentor +} + +export interface MentorshipAppointment { + id?: string; // linked to doc uid in firebase, not field + startTime: number; + endTime: number; + mentorId: string; + hackerId?: string; // a hacker book for the whole team + hackerDescription?: string; // desc given needed by hacker + location: string; + teamName?: string; +} diff --git a/functions/src/models/user.ts b/functions/src/models/user.ts index 267e782..800ecea 100644 --- a/functions/src/models/user.ts +++ b/functions/src/models/user.ts @@ -1,4 +1,6 @@ export interface User { + id?: string; // linked to uid in firebase doc (not in the field) + firstName: string; lastName: string; email: string; diff --git a/functions/src/routes/index.ts b/functions/src/routes/index.ts index 8c9f1ad..3f5ee4a 100644 --- a/functions/src/routes/index.ts +++ b/functions/src/routes/index.ts @@ -3,6 +3,7 @@ import authRoutes from "./auth"; import applicationRoutes from "./application"; import userRoutes from "./user"; import ticketRoutes from "./ticket"; +import mentorshipRoutes from "./mentorship"; const router: Router = express.Router(); @@ -10,5 +11,6 @@ router.use("/auth", authRoutes); router.use("/users", userRoutes); router.use("/application", applicationRoutes) router.use("/tickets", ticketRoutes); +router.use("/mentorship", mentorshipRoutes) export default router; diff --git a/functions/src/server.ts b/functions/src/server.ts index 3cdb290..2ebe26e 100644 --- a/functions/src/server.ts +++ b/functions/src/server.ts @@ -51,6 +51,8 @@ app.use((req: Request, res: Response, next: NextFunction) => { if (req.method === "OPTIONS") { return next(); } + + /** Uncomment to disable session validation */ validateSessionCookie(req, res, next); }); diff --git a/functions/src/utils/fake_data_populator.ts b/functions/src/utils/fake_data_populator.ts index ce6836d..8fb27b0 100644 --- a/functions/src/utils/fake_data_populator.ts +++ b/functions/src/utils/fake_data_populator.ts @@ -6,6 +6,9 @@ import { Question, QUESTION_TYPE, } from "../types/application_types"; +import { FirestoreMentor, MentorshipAppointment } from "../models/mentorship"; +import { dummy_mentors } from "../data/dummy/user"; +import { dummy_mentorships } from "../data/dummy/mentorship"; /** * Logs a message with a specific prefix. @@ -45,6 +48,8 @@ export class FakeDataPopulator { await this.generateUsers(); await this.generateQuestions(); + await this.generateMentorshipAppointments(); + await this.generateMentors() } } @@ -192,6 +197,25 @@ export class FakeDataPopulator { await this.createQuestionDocument(q); } + /** + * Generates mentorship appointments from the given dummy data. + */ + private async generateMentorshipAppointments(): Promise { + log("generateMentorshipAppointments"); + + dummy_mentorships.map(async (dM: MentorshipAppointment) => { + await this.createMentorshipAppointmentDocument(dM) + }) + } + + private async generateMentors(): Promise { + log("generateMentors") + + dummy_mentors.map(async (mentor: FirestoreMentor) => { + await this.createMentorDocument(mentor) + }) + } + /** * Gets the document reference for the generate document. * @returns {firestore.DocumentReference} The document reference. @@ -228,4 +252,19 @@ export class FakeDataPopulator { private async createQuestionDocument(q: Question): Promise { await this.firestoreDatabase.collection("questions").add(q); } + + /** + * Create a mentorship appointment. + * @param mA mentorship appointment + */ + private async createMentorshipAppointmentDocument(mA: MentorshipAppointment): Promise { + await this.firestoreDatabase.collection("mentorships").add(mA) + } + + /** + * Create a mentor in user collection. + */ + private async createMentorDocument(mentor: FirestoreMentor): Promise { + await this.firestoreDatabase.collection("users").add(mentor) + } } From 3bbbfead5d9c3d411b67ba19c0425ecc79a1c4db Mon Sep 17 00:00:00 2001 From: Heryan Djaruma Date: Fri, 18 Jul 2025 10:59:29 +0700 Subject: [PATCH 02/40] add book mentorship endpoint --- .../src/controllers/mentorship_controller.ts | 70 ++++++++++++++++++- functions/src/routes/mentorship.ts | 10 +++ 2 files changed, 77 insertions(+), 3 deletions(-) create mode 100644 functions/src/routes/mentorship.ts diff --git a/functions/src/controllers/mentorship_controller.ts b/functions/src/controllers/mentorship_controller.ts index 5c40b85..6d3ecb7 100644 --- a/functions/src/controllers/mentorship_controller.ts +++ b/functions/src/controllers/mentorship_controller.ts @@ -53,11 +53,10 @@ export const getMentorshipAppointmentsByMentorId = async ( try { const { mentorId } = req.params; const doc = await db.collection("mentorships") - .where("mentor", "==", true) .where("mentorId", "==", mentorId) .get(); - if (!doc.empty) { + if (doc.empty) { res.status(404).json({ error: "Cannot find mentorships related with the mentor." }); return; } @@ -74,4 +73,69 @@ export const getMentorshipAppointmentsByMentorId = async ( } catch (error) { res.status(500).json({ error: (error as Error).message }); } -}; \ No newline at end of file +}; + +export const bookAMentorshipAppointment = async ( + req: Request, + res: Response +): Promise => { + try { + const { mentorshipAppointmentId, hackerId } = req.body + + // reject if no mentorshipAppointmentId + if (mentorshipAppointmentId === undefined || !mentorshipAppointmentId) { + res.status(400).json({ + status: 400, + error: "mentorshipAppointmentId is required in body" + }) + return + } + + // reject if no hackerId + if (hackerId === undefined || !hackerId) { + res.status(400).json({ + status: 400, + error: "hackerId is required in body" + }) + return + } + + /** + * Validation: + * 1. Reject if the mentorship appointment does not exist in db + * 2. Reject if the mentorship is already booked + */ + const mentorshipAppointmentDoc = await db.collection("mentorships").doc(mentorshipAppointmentId) + const mentorshipAppointmentSnap = await mentorshipAppointmentDoc.get() + if (!mentorshipAppointmentSnap.exists) { + res.status(400).json({ + status: 400, + error: "Mentorship appointment does not exist" + }) + } + + const mentorshipData: MentorshipAppointment | undefined = mentorshipAppointmentSnap.data() as MentorshipAppointment + if (mentorshipData.hackerId) { + res.status(400).json({ + status: 400, + error: "Mentorship slot is already booked" + }) + } + + /** + * Book the mentorship slot + * 1. Update the hackerId for the document + */ + const updatedMentorshipAppointment = { + hackerId: hackerId, + ...mentorshipData + } + await mentorshipAppointmentDoc.update(updatedMentorshipAppointment) + res.status(200).json({ + status: 200, + data: "Successfuly booked mentorship slot" + }) + } catch (error) { + res.status(500).json({error: (error as Error).message}) + } +} \ No newline at end of file diff --git a/functions/src/routes/mentorship.ts b/functions/src/routes/mentorship.ts new file mode 100644 index 0000000..1222ed5 --- /dev/null +++ b/functions/src/routes/mentorship.ts @@ -0,0 +1,10 @@ +import express, { Request, Response } from "express"; +import { bookAMentorshipAppointment, getMentors, getMentorshipAppointmentsByMentorId } from "../controllers/mentorship_controller"; + +const router = express.Router(); + +router.get("/mentors", (req: Request, res: Response) => getMentors(req, res)) +router.get("/mentorships/:mentorId", (req: Request, res: Response) => getMentorshipAppointmentsByMentorId(req, res)) +router.post("/mentorships", (req: Request, res: Response) => bookAMentorshipAppointment(req, res)) + +export default router; \ No newline at end of file From 2ffce10c770a949b06b123d0145ff57111d0018b Mon Sep 17 00:00:00 2001 From: Heryan Djaruma Date: Fri, 18 Jul 2025 11:37:46 +0700 Subject: [PATCH 03/40] add get my mentorships --- .../src/controllers/mentorship_controller.ts | 114 ++++++++++++++++-- functions/src/routes/mentorship.ts | 3 +- 2 files changed, 103 insertions(+), 14 deletions(-) diff --git a/functions/src/controllers/mentorship_controller.ts b/functions/src/controllers/mentorship_controller.ts index 6d3ecb7..1087ae8 100644 --- a/functions/src/controllers/mentorship_controller.ts +++ b/functions/src/controllers/mentorship_controller.ts @@ -1,6 +1,7 @@ import { db } from "../config/firebase" import { FirestoreMentor, MentorshipAppointment } from "../models/mentorship"; import { Request, Response } from "express"; +import { User } from "../models/user"; export const getMentors = async ( req: Request, @@ -80,7 +81,21 @@ export const bookAMentorshipAppointment = async ( res: Response ): Promise => { try { - const { mentorshipAppointmentId, hackerId } = req.body + /** + * Validate current user: + * 1. User is not valid / not authenticated. + * 2. Get current user from db + */ + const uid = req.user?.uid; + if (!uid || uid === undefined) { + res.status(401).json({ + status: 401, + error: "Cannot get current user uid" + }) + return; + } + + const { mentorshipAppointmentId } = req.body // reject if no mentorshipAppointmentId if (mentorshipAppointmentId === undefined || !mentorshipAppointmentId) { @@ -91,15 +106,6 @@ export const bookAMentorshipAppointment = async ( return } - // reject if no hackerId - if (hackerId === undefined || !hackerId) { - res.status(400).json({ - status: 400, - error: "hackerId is required in body" - }) - return - } - /** * Validation: * 1. Reject if the mentorship appointment does not exist in db @@ -112,6 +118,7 @@ export const bookAMentorshipAppointment = async ( status: 400, error: "Mentorship appointment does not exist" }) + return } const mentorshipData: MentorshipAppointment | undefined = mentorshipAppointmentSnap.data() as MentorshipAppointment @@ -120,6 +127,7 @@ export const bookAMentorshipAppointment = async ( status: 400, error: "Mentorship slot is already booked" }) + return } /** @@ -127,15 +135,95 @@ export const bookAMentorshipAppointment = async ( * 1. Update the hackerId for the document */ const updatedMentorshipAppointment = { - hackerId: hackerId, + hackerId: uid, ...mentorshipData - } + } await mentorshipAppointmentDoc.update(updatedMentorshipAppointment) res.status(200).json({ status: 200, data: "Successfuly booked mentorship slot" }) } catch (error) { - res.status(500).json({error: (error as Error).message}) + res.status(500).json({ error: (error as Error).message }) } +} + + +/** + * Get the list of my mentorship appointments. + * If user is mentor, by default will return his appointment. + */ +export const getMyMentorshipAppointments = async ( + req: Request, + res: Response +): Promise => { + try { + /** + * Validate current user: + * 1. User is not valid / not authenticated. + * 2. Get current user from db + */ + const uid = req.user?.uid; + if (!uid || uid === undefined) { + res.status(401).json({ + status: 401, + error: "Cannot get current user uid" + }) + return; + } + + const currentUserDoc = await db.collection("users").doc(uid) + const currentUserSnap = await currentUserDoc.get() + if (!currentUserSnap.exists) { + res.status(400).json({ + status: 400, + error: "Cannot find user in the users collection" + }) + return; + } + + /** + * Get user mentorship appointments: + * 1. Check if user is mentor, then fetch his/her appointments + * 2. Otherwise return appointments with current user id as hackerId + */ + let mentorshipAppointments: MentorshipAppointment[] = [] + const currentUserData = currentUserSnap.data() as FirestoreMentor | User + if (isMentor(currentUserData)) { + const appointmentsAsMentor = await db.collection("mentorships") + .where("mentorId", "==", uid) + .get() + mentorshipAppointments = appointmentsAsMentor.docs.map((appointment) => ({ + id: appointment.id, + ...appointment.data() + })) as MentorshipAppointment[]; + + res.status(200).json({ + status: 200, + data: appointmentsAsMentor + }) + return; + } + else { + const appointmentsAsHacker = await db.collection("mentorships") + .where("hackerId", "==", uid) + .get() + + mentorshipAppointments = appointmentsAsHacker.docs.map((appointment) => ({ + id: appointment.id, + ...appointment.data() + })) as MentorshipAppointment[] + } + + res.status(200).json({ + status: 200, + data: mentorshipAppointments + }) + } catch (error) { + res.status(500).json({ error: (error as Error).message }) + } +} + +function isMentor(data: FirestoreMentor | User): data is FirestoreMentor { + return 'mentor' in data; } \ No newline at end of file diff --git a/functions/src/routes/mentorship.ts b/functions/src/routes/mentorship.ts index 1222ed5..6d3f700 100644 --- a/functions/src/routes/mentorship.ts +++ b/functions/src/routes/mentorship.ts @@ -1,10 +1,11 @@ import express, { Request, Response } from "express"; -import { bookAMentorshipAppointment, getMentors, getMentorshipAppointmentsByMentorId } from "../controllers/mentorship_controller"; +import { bookAMentorshipAppointment, getMentors, getMentorshipAppointmentsByMentorId, getMyMentorshipAppointments } from "../controllers/mentorship_controller"; const router = express.Router(); router.get("/mentors", (req: Request, res: Response) => getMentors(req, res)) router.get("/mentorships/:mentorId", (req: Request, res: Response) => getMentorshipAppointmentsByMentorId(req, res)) router.post("/mentorships", (req: Request, res: Response) => bookAMentorshipAppointment(req, res)) +router.get("/my-mentorships", (req: Request, res: Response) => getMyMentorshipAppointments(req, res)) export default router; \ No newline at end of file From c06a770fcaeab6b457e22a62f85ed1e9282a7f56 Mon Sep 17 00:00:00 2001 From: Heryan Djaruma Date: Fri, 18 Jul 2025 17:23:17 +0700 Subject: [PATCH 04/40] add get mentor by mentorId --- .../src/controllers/mentorship_controller.ts | 70 +++++++++++++++++-- functions/src/data/dummy/mentorship.ts | 2 +- functions/src/data/dummy/user.ts | 11 +-- functions/src/routes/mentorship.ts | 3 +- functions/src/utils/fake_data_populator.ts | 13 ++-- 5 files changed, 78 insertions(+), 21 deletions(-) diff --git a/functions/src/controllers/mentorship_controller.ts b/functions/src/controllers/mentorship_controller.ts index 1087ae8..fb5b372 100644 --- a/functions/src/controllers/mentorship_controller.ts +++ b/functions/src/controllers/mentorship_controller.ts @@ -3,11 +3,68 @@ import { FirestoreMentor, MentorshipAppointment } from "../models/mentorship"; import { Request, Response } from "express"; import { User } from "../models/user"; +export const getMentor = async ( + req: Request, + res: Response +): Promise => { + const { mentorId } = req.params; + try { + /** + * Validate request: + * 1. Mentor id is not in params + */ + if (!mentorId) { + res.status(400).json({ + status: 400, + error: "Mentor id is required" + }) + return; + } + + /** + * Process request + * 1. Find mentor in db + */ + const mentorDoc = await db.collection("users").doc(mentorId).get() + if (!mentorDoc.exists) { + res.status(404).json({ + status: 404, + error: "Cannot find mentor with the given mentor id" + }) + return; + } + const mentorData = mentorDoc.data() + + if (mentorData) { + const trimmedData: FirestoreMentor = { + "id": mentorData.id, + "email": mentorData.email, + "name": mentorData.name, + "intro": mentorData.intro, + "specialization": mentorData.specialization, + "mentor": mentorData.mentor, + "discordUsername": mentorData.discordUsername + } + res.status(200).json({ + status: 200, + data: trimmedData + }) + } else { + res.status(200).json({ + status: 200, + data: {} + }) + } + } catch (error) { + res.status(500).json({ error: (error as Error).message }) + } +} + export const getMentors = async ( req: Request, res: Response ): Promise => { - let allMentors: FirestoreMentor[] = []; + const allMentors: FirestoreMentor[] = []; try { const snapshot = await db.collection('users') @@ -29,7 +86,7 @@ export const getMentorshipAppointments = async ( req: Request, res: Response ): Promise => { - let mentorshipAppointments: MentorshipAppointment[] = []; + const mentorshipAppointments: MentorshipAppointment[] = []; try { const snapshot = await db.collection('mentorships') @@ -62,7 +119,7 @@ export const getMentorshipAppointmentsByMentorId = async ( return; } - let mentorships: MentorshipAppointment[] = []; + const mentorships: MentorshipAppointment[] = []; doc.docs.map((mentorship) => { mentorships.push({ id: mentorship.id, @@ -208,7 +265,7 @@ export const getMyMentorshipAppointments = async ( const appointmentsAsHacker = await db.collection("mentorships") .where("hackerId", "==", uid) .get() - + mentorshipAppointments = appointmentsAsHacker.docs.map((appointment) => ({ id: appointment.id, ...appointment.data() @@ -224,6 +281,11 @@ export const getMyMentorshipAppointments = async ( } } +/** + * + * @param data + * @returns + */ function isMentor(data: FirestoreMentor | User): data is FirestoreMentor { return 'mentor' in data; } \ No newline at end of file diff --git a/functions/src/data/dummy/mentorship.ts b/functions/src/data/dummy/mentorship.ts index 5758c88..9e9825f 100644 --- a/functions/src/data/dummy/mentorship.ts +++ b/functions/src/data/dummy/mentorship.ts @@ -1,6 +1,6 @@ import { MentorshipAppointment } from "../../models/mentorship"; -export const dummy_mentorships: MentorshipAppointment[] = [ +export const dummyMentorships: MentorshipAppointment[] = [ { "mentorId": "-OVQFbrn-Ct6u6SSB47I", "startTime": 1752804000, diff --git a/functions/src/data/dummy/user.ts b/functions/src/data/dummy/user.ts index dc18b69..b457c7f 100644 --- a/functions/src/data/dummy/user.ts +++ b/functions/src/data/dummy/user.ts @@ -1,7 +1,7 @@ import { FirestoreMentor } from "../../models/mentorship"; import { User } from "../../models/user"; -export const dummy_mentors: FirestoreMentor[] = [ +export const dummyMentors: FirestoreMentor[] = [ { "id": "-OVQFbrn-Ct6u6SSB47I", mentor: true, @@ -20,13 +20,4 @@ export const dummy_mentors: FirestoreMentor[] = [ "discordUsername": "hehei", "intro": "Hi guuyss" } -] - -export const dummy_hackers: User[] = [ - { - "id": "-OVQHbXAlQiM5NQHrTe1", - "email": "hacker@gmail.com", - "firstName": "Hacker", - "lastName": "Last", - } ] \ No newline at end of file diff --git a/functions/src/routes/mentorship.ts b/functions/src/routes/mentorship.ts index 6d3f700..420ed66 100644 --- a/functions/src/routes/mentorship.ts +++ b/functions/src/routes/mentorship.ts @@ -1,9 +1,10 @@ import express, { Request, Response } from "express"; -import { bookAMentorshipAppointment, getMentors, getMentorshipAppointmentsByMentorId, getMyMentorshipAppointments } from "../controllers/mentorship_controller"; +import { bookAMentorshipAppointment, getMentor, getMentors, getMentorshipAppointmentsByMentorId, getMyMentorshipAppointments } from "../controllers/mentorship_controller"; const router = express.Router(); router.get("/mentors", (req: Request, res: Response) => getMentors(req, res)) +router.get("/mentors/:mentorId", (req: Request, res: Response) => getMentor(req, res)) router.get("/mentorships/:mentorId", (req: Request, res: Response) => getMentorshipAppointmentsByMentorId(req, res)) router.post("/mentorships", (req: Request, res: Response) => bookAMentorshipAppointment(req, res)) router.get("/my-mentorships", (req: Request, res: Response) => getMyMentorshipAppointments(req, res)) diff --git a/functions/src/utils/fake_data_populator.ts b/functions/src/utils/fake_data_populator.ts index 8fb27b0..d0db596 100644 --- a/functions/src/utils/fake_data_populator.ts +++ b/functions/src/utils/fake_data_populator.ts @@ -7,8 +7,8 @@ import { QUESTION_TYPE, } from "../types/application_types"; import { FirestoreMentor, MentorshipAppointment } from "../models/mentorship"; -import { dummy_mentors } from "../data/dummy/user"; -import { dummy_mentorships } from "../data/dummy/mentorship"; +import { dummyMentors } from "../data/dummy/user"; +import { dummyMentorships } from "../data/dummy/mentorship"; /** * Logs a message with a specific prefix. @@ -203,15 +203,18 @@ export class FakeDataPopulator { private async generateMentorshipAppointments(): Promise { log("generateMentorshipAppointments"); - dummy_mentorships.map(async (dM: MentorshipAppointment) => { + dummyMentorships.map(async (dM: MentorshipAppointment) => { await this.createMentorshipAppointmentDocument(dM) }) } - + + /** + * Generate fake mentors. + */ private async generateMentors(): Promise { log("generateMentors") - dummy_mentors.map(async (mentor: FirestoreMentor) => { + dummyMentors.map(async (mentor: FirestoreMentor) => { await this.createMentorDocument(mentor) }) } From 29c59978579a47a716dc5ec2a336c3d380b26cbb Mon Sep 17 00:00:00 2001 From: Heryan Djaruma Date: Fri, 18 Jul 2025 17:44:31 +0700 Subject: [PATCH 05/40] add get role endpoint --- functions/src/controllers/auth_controller.ts | 46 ++++++++++++++++++++ functions/src/routes/auth.ts | 2 + 2 files changed, 48 insertions(+) diff --git a/functions/src/controllers/auth_controller.ts b/functions/src/controllers/auth_controller.ts index aa87dd9..b10cb38 100644 --- a/functions/src/controllers/auth_controller.ts +++ b/functions/src/controllers/auth_controller.ts @@ -715,3 +715,49 @@ export const verifyAccount = async ( }); } }; + +export const getCurrentUserRole = async ( + req: Request, + res: Response +): Promise => { + try { + /** + * Check request: + * 1. Cookie must be present in header. + */ + const uid = req.user?.uid + if (req.user === undefined) { + res.status(401).json({ + status: 401, + error: "Unauthorized" + }) + return; + } + if (!uid || uid === undefined) { + res.status(401).json({ + status: 401, + error: "Unauthorized" + }) + return; + } + + /** + * Process request: + * 1. Get claims of a user. If none, then default to hacker. + */ + if (req.user.mentor === true) { + res.status(200).json({ + status: 200, + role: "mentor" + }) + return; + } + + res.status(200).json({ + status: 200, + role: "hacker" + }) + } catch (error) { + res.status(500).json({ error: (error as Error).message }) + } +} \ No newline at end of file diff --git a/functions/src/routes/auth.ts b/functions/src/routes/auth.ts index c9cbc75..6941c56 100644 --- a/functions/src/routes/auth.ts +++ b/functions/src/routes/auth.ts @@ -1,5 +1,6 @@ import express, { Request, Response } from "express"; import { + getCurrentUserRole, login, logout, register, @@ -11,6 +12,7 @@ import { const router = express.Router(); +router.get("/role", (req: Request, res: Response) => getCurrentUserRole(req, res)) router.post("/login", (req: Request, res: Response) => login(req, res)); router.post("/register", (req: Request, res: Response) => register(req, res)); router.post("/reset-password", requestPasswordReset); From 03a4c11a9438eb4f8bf13015b8595d4a100f867e Mon Sep 17 00:00:00 2001 From: Heryan Djaruma Date: Fri, 18 Jul 2025 23:27:11 +0700 Subject: [PATCH 06/40] add mentorship config endpoint --- .../src/controllers/mentorship_controller.ts | 25 +++++++++++++++++++ functions/src/routes/mentorship.ts | 3 ++- functions/src/types/config.ts | 5 ++++ 3 files changed, 32 insertions(+), 1 deletion(-) create mode 100644 functions/src/types/config.ts diff --git a/functions/src/controllers/mentorship_controller.ts b/functions/src/controllers/mentorship_controller.ts index fb5b372..7868044 100644 --- a/functions/src/controllers/mentorship_controller.ts +++ b/functions/src/controllers/mentorship_controller.ts @@ -2,6 +2,31 @@ import { db } from "../config/firebase" import { FirestoreMentor, MentorshipAppointment } from "../models/mentorship"; import { Request, Response } from "express"; import { User } from "../models/user"; +import { MentorshipConfig } from "../types/config"; + +export const getMentorshipConfig = async ( + req: Request, + res: Response +): Promise => { + try { + const mentorshipConfigSnapshot = await db.collection("config").doc("mentorshipConfig").get() + const mentorshipConfigData = mentorshipConfigSnapshot.data() + + if (!mentorshipConfigSnapshot.exists || mentorshipConfigData === undefined) { + res.status(400).json({ + status: 400, + error: "Config not found" + }) + return; + } + res.status(200).json({ + status: 200, + data: mentorshipConfigData + }) + } catch (error) { + res.status(500).json({error: (error as Error).message}) + } +} export const getMentor = async ( req: Request, diff --git a/functions/src/routes/mentorship.ts b/functions/src/routes/mentorship.ts index 420ed66..20d558e 100644 --- a/functions/src/routes/mentorship.ts +++ b/functions/src/routes/mentorship.ts @@ -1,8 +1,9 @@ import express, { Request, Response } from "express"; -import { bookAMentorshipAppointment, getMentor, getMentors, getMentorshipAppointmentsByMentorId, getMyMentorshipAppointments } from "../controllers/mentorship_controller"; +import { bookAMentorshipAppointment, getMentor, getMentors, getMentorshipAppointmentsByMentorId, getMentorshipConfig, getMyMentorshipAppointments } from "../controllers/mentorship_controller"; const router = express.Router(); +router.get("/config", (req: Request, res: Response) => getMentorshipConfig(req, res)) router.get("/mentors", (req: Request, res: Response) => getMentors(req, res)) router.get("/mentors/:mentorId", (req: Request, res: Response) => getMentor(req, res)) router.get("/mentorships/:mentorId", (req: Request, res: Response) => getMentorshipAppointmentsByMentorId(req, res)) diff --git a/functions/src/types/config.ts b/functions/src/types/config.ts new file mode 100644 index 0000000..d07d1bc --- /dev/null +++ b/functions/src/types/config.ts @@ -0,0 +1,5 @@ +export interface MentorshipConfig { + isMentorshipOpen: boolean; + mentoringStart: number; + mentoringEnd: number; +} \ No newline at end of file From da0f5c30360cc054b03c31e69771ffc3689db41b Mon Sep 17 00:00:00 2001 From: Heryan Djaruma Date: Sat, 19 Jul 2025 02:30:40 +0700 Subject: [PATCH 07/40] minor fix controller --- functions/src/controllers/mentorship_controller.ts | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/functions/src/controllers/mentorship_controller.ts b/functions/src/controllers/mentorship_controller.ts index 7868044..d9fea8c 100644 --- a/functions/src/controllers/mentorship_controller.ts +++ b/functions/src/controllers/mentorship_controller.ts @@ -177,7 +177,7 @@ export const bookAMentorshipAppointment = async ( return; } - const { mentorshipAppointmentId } = req.body + const { mentorshipAppointmentId, hackerDescription } = req.body // reject if no mentorshipAppointmentId if (mentorshipAppointmentId === undefined || !mentorshipAppointmentId) { @@ -218,6 +218,7 @@ export const bookAMentorshipAppointment = async ( */ const updatedMentorshipAppointment = { hackerId: uid, + hackerDescription: hackerDescription, ...mentorshipData } await mentorshipAppointmentDoc.update(updatedMentorshipAppointment) @@ -282,7 +283,7 @@ export const getMyMentorshipAppointments = async ( res.status(200).json({ status: 200, - data: appointmentsAsMentor + data: mentorshipAppointments }) return; } From 5e808ca8d2cf1026b0b9a4b59b9853989304701b Mon Sep 17 00:00:00 2001 From: Heryan Djaruma Date: Sat, 19 Jul 2025 15:00:58 +0700 Subject: [PATCH 08/40] add upcomingOnly --- .../src/controllers/mentorship_controller.ts | 58 +++++++++++++++++-- 1 file changed, 53 insertions(+), 5 deletions(-) diff --git a/functions/src/controllers/mentorship_controller.ts b/functions/src/controllers/mentorship_controller.ts index d9fea8c..2129627 100644 --- a/functions/src/controllers/mentorship_controller.ts +++ b/functions/src/controllers/mentorship_controller.ts @@ -24,7 +24,7 @@ export const getMentorshipConfig = async ( data: mentorshipConfigData }) } catch (error) { - res.status(500).json({error: (error as Error).message}) + res.status(500).json({ error: (error as Error).message }) } } @@ -107,6 +107,14 @@ export const getMentors = async ( } } + +/** + * Get all mentorship appointments. + * + * To be used in admin. + * @param req + * @param res + */ export const getMentorshipAppointments = async ( req: Request, res: Response @@ -129,6 +137,17 @@ export const getMentorshipAppointments = async ( } } +/** + * Get mentorship appointments by mentor id. + * + * Use for: + * 1. Admin + * 2. Mentor in portal + * 3. Hacker in portal + * @param req + * @param res + * @returns + */ export const getMentorshipAppointmentsByMentorId = async ( req: Request, res: Response @@ -158,6 +177,15 @@ export const getMentorshipAppointmentsByMentorId = async ( } }; +/** + * Book mentorship appointment. Use request cookie to get hacker uid. + * + * Use for: + * 1. Hacker in portal + * @param req + * @param res + * @returns + */ export const bookAMentorshipAppointment = async ( req: Request, res: Response @@ -235,6 +263,11 @@ export const bookAMentorshipAppointment = async ( /** * Get the list of my mentorship appointments. * If user is mentor, by default will return his appointment. + * Use request cookie to get uid. + * + * Use for: + * 1. Mentor in portal + * 2. Hacker in portal */ export const getMyMentorshipAppointments = async ( req: Request, @@ -265,6 +298,11 @@ export const getMyMentorshipAppointments = async ( return; } + /** + * Get argument to fetch only recentOnly to true. + */ + const upcomingOnly = req.params.recentOnly + /** * Get user mentorship appointments: * 1. Check if user is mentor, then fetch his/her appointments @@ -273,9 +311,14 @@ export const getMyMentorshipAppointments = async ( let mentorshipAppointments: MentorshipAppointment[] = [] const currentUserData = currentUserSnap.data() as FirestoreMentor | User if (isMentor(currentUserData)) { - const appointmentsAsMentor = await db.collection("mentorships") + const appointmentsAsMentorQuery = await db.collection("mentorships") .where("mentorId", "==", uid) - .get() + + if (upcomingOnly === "true") { + appointmentsAsMentorQuery.where("startTime", ">=", new Date()) + } + + const appointmentsAsMentor = await appointmentsAsMentorQuery.orderBy("startTime", "asc").get() mentorshipAppointments = appointmentsAsMentor.docs.map((appointment) => ({ id: appointment.id, ...appointment.data() @@ -288,9 +331,14 @@ export const getMyMentorshipAppointments = async ( return; } else { - const appointmentsAsHacker = await db.collection("mentorships") + const appointmentsAsHackerQuery = await db.collection("mentorships") .where("hackerId", "==", uid) - .get() + + if (upcomingOnly === "true") { + appointmentsAsHackerQuery.where("startTime", ">=", new Date()) + } + + const appointmentsAsHacker = await appointmentsAsHackerQuery.orderBy("startTime", "asc").get() mentorshipAppointments = appointmentsAsHacker.docs.map((appointment) => ({ id: appointment.id, From 9e44d9b40e845f6cff57017f7f8c9746582b19c7 Mon Sep 17 00:00:00 2001 From: Heryan Djaruma Date: Sat, 19 Jul 2025 15:49:24 +0700 Subject: [PATCH 09/40] add luxon, handle get upcoming mentorships only --- functions/package-lock.json | 18 ++++ functions/package.json | 2 + .../src/controllers/mentorship_controller.ts | 90 ++++++------------- 3 files changed, 49 insertions(+), 61 deletions(-) diff --git a/functions/package-lock.json b/functions/package-lock.json index b0acd38..05efad6 100644 --- a/functions/package-lock.json +++ b/functions/package-lock.json @@ -16,6 +16,7 @@ "firebase-functions": "^6.3.2", "jsonwebtoken": "^9.0.2", "lodash": "^4.17.21", + "luxon": "^3.7.1", "nodemailer": "^7.0.3", "validator": "^13.15.0" }, @@ -28,6 +29,7 @@ "@types/express": "^5.0.1", "@types/jsonwebtoken": "^9.0.9", "@types/lodash": "^4.17.16", + "@types/luxon": "^3.6.2", "@types/nodemailer": "^6.4.17", "@types/validator": "^13.12.2", "@typescript-eslint/eslint-plugin": "^5.62.0", @@ -1825,6 +1827,13 @@ "license": "MIT", "optional": true }, + "node_modules/@types/luxon": { + "version": "3.6.2", + "resolved": "https://registry.npmjs.org/@types/luxon/-/luxon-3.6.2.tgz", + "integrity": "sha512-R/BdP7OxEMc44l2Ex5lSXHoIXTB2JLNa3y2QISIbr58U/YcsffyQrYW//hZSdrfxrjRZj3GcUoxMPGdO8gSYuw==", + "dev": true, + "license": "MIT" + }, "node_modules/@types/mime": { "version": "1.3.5", "resolved": "https://registry.npmjs.org/@types/mime/-/mime-1.3.5.tgz", @@ -6774,6 +6783,15 @@ "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==", "license": "ISC" }, + "node_modules/luxon": { + "version": "3.7.1", + "resolved": "https://registry.npmjs.org/luxon/-/luxon-3.7.1.tgz", + "integrity": "sha512-RkRWjA926cTvz5rAb1BqyWkKbbjzCGchDUIKMCUvNi17j6f6j8uHGDV82Aqcqtzd+icoYpELmG3ksgGiFNNcNg==", + "license": "MIT", + "engines": { + "node": ">=12" + } + }, "node_modules/make-dir": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/make-dir/-/make-dir-4.0.0.tgz", diff --git a/functions/package.json b/functions/package.json index 55b50ae..7c91308 100644 --- a/functions/package.json +++ b/functions/package.json @@ -26,6 +26,7 @@ "firebase-functions": "^6.3.2", "jsonwebtoken": "^9.0.2", "lodash": "^4.17.21", + "luxon": "^3.7.1", "nodemailer": "^7.0.3", "validator": "^13.15.0" }, @@ -38,6 +39,7 @@ "@types/express": "^5.0.1", "@types/jsonwebtoken": "^9.0.9", "@types/lodash": "^4.17.16", + "@types/luxon": "^3.6.2", "@types/nodemailer": "^6.4.17", "@types/validator": "^13.12.2", "@typescript-eslint/eslint-plugin": "^5.62.0", diff --git a/functions/src/controllers/mentorship_controller.ts b/functions/src/controllers/mentorship_controller.ts index 2129627..aecfb97 100644 --- a/functions/src/controllers/mentorship_controller.ts +++ b/functions/src/controllers/mentorship_controller.ts @@ -2,7 +2,9 @@ import { db } from "../config/firebase" import { FirestoreMentor, MentorshipAppointment } from "../models/mentorship"; import { Request, Response } from "express"; import { User } from "../models/user"; +import { DateTime } from 'luxon'; import { MentorshipConfig } from "../types/config"; +import { CollectionReference, DocumentData } from "firebase-admin/firestore"; export const getMentorshipConfig = async ( req: Request, @@ -274,86 +276,52 @@ export const getMyMentorshipAppointments = async ( res: Response ): Promise => { try { - /** - * Validate current user: - * 1. User is not valid / not authenticated. - * 2. Get current user from db - */ + // 1. Validate User const uid = req.user?.uid; - if (!uid || uid === undefined) { - res.status(401).json({ - status: 401, - error: "Cannot get current user uid" - }) + if (!uid) { + res.status(401).json({ status: 401, error: "Cannot get current user uid" }); return; } - const currentUserDoc = await db.collection("users").doc(uid) - const currentUserSnap = await currentUserDoc.get() + const currentUserSnap = await db.collection("users").doc(uid).get(); if (!currentUserSnap.exists) { - res.status(400).json({ - status: 400, - error: "Cannot find user in the users collection" - }) + res.status(400).json({ status: 400, error: "User not found" }); return; } - /** - * Get argument to fetch only recentOnly to true. - */ - const upcomingOnly = req.params.recentOnly + const currentUserData = currentUserSnap.data() as FirestoreMentor | User; + const upcomingOnly = req.query.upcomingOnly === 'true'; - /** - * Get user mentorship appointments: - * 1. Check if user is mentor, then fetch his/her appointments - * 2. Otherwise return appointments with current user id as hackerId - */ - let mentorshipAppointments: MentorshipAppointment[] = [] - const currentUserData = currentUserSnap.data() as FirestoreMentor | User - if (isMentor(currentUserData)) { - const appointmentsAsMentorQuery = await db.collection("mentorships") - .where("mentorId", "==", uid) + // 2. Build Query Dynamically + let query = db.collection("mentorships"); - if (upcomingOnly === "true") { - appointmentsAsMentorQuery.where("startTime", ">=", new Date()) - } - - const appointmentsAsMentor = await appointmentsAsMentorQuery.orderBy("startTime", "asc").get() - mentorshipAppointments = appointmentsAsMentor.docs.map((appointment) => ({ - id: appointment.id, - ...appointment.data() - })) as MentorshipAppointment[]; - - res.status(200).json({ - status: 200, - data: mentorshipAppointments - }) - return; + if (isMentor(currentUserData)) { + query = query.where("mentorId", "==", uid) as CollectionReference; + } else { + query = query.where("hackerId", "==", uid) as CollectionReference;; } - else { - const appointmentsAsHackerQuery = await db.collection("mentorships") - .where("hackerId", "==", uid) - if (upcomingOnly === "true") { - appointmentsAsHackerQuery.where("startTime", ">=", new Date()) - } + if (upcomingOnly) { + const currentTimeSeconds = Math.floor(DateTime.now().setZone('Asia/Jakarta').toUnixInteger()); + query = query.where("startTime", ">=", currentTimeSeconds) as CollectionReference;; + } - const appointmentsAsHacker = await appointmentsAsHackerQuery.orderBy("startTime", "asc").get() + // 3. Execute Query and Send Response + const snapshot = await query.orderBy("startTime", "asc").get(); - mentorshipAppointments = appointmentsAsHacker.docs.map((appointment) => ({ - id: appointment.id, - ...appointment.data() - })) as MentorshipAppointment[] - } + const mentorshipAppointments = snapshot.docs.map((doc) => ({ + id: doc.id, + ...doc.data(), + })) as MentorshipAppointment[]; res.status(200).json({ status: 200, - data: mentorshipAppointments - }) + data: mentorshipAppointments, + }); } catch (error) { - res.status(500).json({ error: (error as Error).message }) + res.status(500).json({ error: (error as Error).message }); } -} +}; /** * From 8fc55dfeda4814d9c1de6efd87b3f60349890ced Mon Sep 17 00:00:00 2001 From: Heryan Djaruma Date: Sat, 19 Jul 2025 17:39:31 +0700 Subject: [PATCH 10/40] add upcomingOnly and recentOnly --- functions/src/controllers/mentorship_controller.ts | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/functions/src/controllers/mentorship_controller.ts b/functions/src/controllers/mentorship_controller.ts index aecfb97..0eee7a8 100644 --- a/functions/src/controllers/mentorship_controller.ts +++ b/functions/src/controllers/mentorship_controller.ts @@ -270,7 +270,11 @@ export const bookAMentorshipAppointment = async ( * Use for: * 1. Mentor in portal * 2. Hacker in portal + * + * @param req.query.upcomingOnly boolean : If or not only fetches upcoming only. + * @param req.query.recentOnly boolean : If or not only fetches recent only. */ + export const getMyMentorshipAppointments = async ( req: Request, res: Response @@ -291,6 +295,7 @@ export const getMyMentorshipAppointments = async ( const currentUserData = currentUserSnap.data() as FirestoreMentor | User; const upcomingOnly = req.query.upcomingOnly === 'true'; + const recentOnly = req.query.recentOnly === 'true'; // 2. Build Query Dynamically let query = db.collection("mentorships"); @@ -301,9 +306,11 @@ export const getMyMentorshipAppointments = async ( query = query.where("hackerId", "==", uid) as CollectionReference;; } + const currentTimeSeconds = Math.floor(DateTime.now().setZone('Asia/Jakarta').toUnixInteger()); if (upcomingOnly) { - const currentTimeSeconds = Math.floor(DateTime.now().setZone('Asia/Jakarta').toUnixInteger()); - query = query.where("startTime", ">=", currentTimeSeconds) as CollectionReference;; + query = query.where("startTime", ">=", currentTimeSeconds) as CollectionReference; + } else if (recentOnly) { + query = query.where("startTime", "<=", currentTimeSeconds) as CollectionReference; } // 3. Execute Query and Send Response From 0f8ec49f2ab9f669bd6f9d7c50fcc421f51b08c8 Mon Sep 17 00:00:00 2001 From: Heryan Djaruma Date: Sun, 20 Jul 2025 14:24:14 +0700 Subject: [PATCH 11/40] edit model and types --- functions/src/models/mentorship.ts | 38 ++++++++++++++++++++++++++++-- functions/src/types/config.ts | 2 -- 2 files changed, 36 insertions(+), 4 deletions(-) diff --git a/functions/src/models/mentorship.ts b/functions/src/models/mentorship.ts index 6bda678..0f8d90a 100644 --- a/functions/src/models/mentorship.ts +++ b/functions/src/models/mentorship.ts @@ -9,12 +9,46 @@ export interface FirestoreMentor { } export interface MentorshipAppointment { - id?: string; // linked to doc uid in firebase, not field + id?: string; startTime: number; endTime: number; mentorId: string; hackerId?: string; // a hacker book for the whole team + teamName: string; hackerDescription?: string; // desc given needed by hacker location: string; - teamName?: string; + offlineLocation?: string; // to be filled if the location is offline + mentorMarkAsDone: boolean; + mentorMarkAsAfk: boolean; // mark if this team is AFK + mentorNotes: string // to give this appointment a note + hackerMarkAsDone: boolean; +} + +export interface MentorshipAppointmentResponseAsMentor { + id?: string; + startTime: number; + endTime: number; + mentorId: string; + hackerId?: string; + hackerName: string; + teamName: string; + hackerDescription?: string; // desc given needed by hacker + location: string; // offline or online + offlineLocation?: string; // to be filled if the location is offline + mentorMarkAsDone: boolean; + mentorMarkAsAfk: boolean; // mark if this team is AFK + mentorNotes: string // to give this appointment a note +} + +export interface MentorshipAppointmentResponseAsHacker { + id?: string; + startTime: number; + endTime: number; + mentorId: string; + hackerId?: string; + hackerName: string; + teamName: string; + hackerDescription?: string; // desc given needed by hacker + location: string; // offline or online + offlineLocation?: string; // to be filled if the location is offline } diff --git a/functions/src/types/config.ts b/functions/src/types/config.ts index d07d1bc..0435e04 100644 --- a/functions/src/types/config.ts +++ b/functions/src/types/config.ts @@ -1,5 +1,3 @@ export interface MentorshipConfig { isMentorshipOpen: boolean; - mentoringStart: number; - mentoringEnd: number; } \ No newline at end of file From d04fe5a2a8f494a47b21dff383dc834d5d309fc5 Mon Sep 17 00:00:00 2001 From: Heryan Djaruma Date: Sun, 20 Jul 2025 15:37:21 +0700 Subject: [PATCH 12/40] add isMentor middleware --- functions/package-lock.json | 8 +++--- functions/package.json | 2 +- functions/src/middlewares/auth_middleware.ts | 15 +++-------- functions/src/middlewares/role_middleware.ts | 28 +++++++++----------- functions/tsconfig.json | 5 ++-- 5 files changed, 24 insertions(+), 34 deletions(-) diff --git a/functions/package-lock.json b/functions/package-lock.json index 05efad6..69facc0 100644 --- a/functions/package-lock.json +++ b/functions/package-lock.json @@ -23,7 +23,7 @@ "devDependencies": { "@faker-js/faker": "^9.6.0", "@types/busboy": "^1.5.4", - "@types/cookie-parser": "^1.4.8", + "@types/cookie-parser": "^1.4.9", "@types/cors": "^2.8.18", "@types/csurf": "^1.11.5", "@types/express": "^5.0.1", @@ -1689,9 +1689,9 @@ } }, "node_modules/@types/cookie-parser": { - "version": "1.4.8", - "resolved": "https://registry.npmjs.org/@types/cookie-parser/-/cookie-parser-1.4.8.tgz", - "integrity": "sha512-l37JqFrOJ9yQfRQkljb41l0xVphc7kg5JTjjr+pLRZ0IyZ49V4BQ8vbF4Ut2C2e+WH4al3xD3ZwYwIUfnbT4NQ==", + "version": "1.4.9", + "resolved": "https://registry.npmjs.org/@types/cookie-parser/-/cookie-parser-1.4.9.tgz", + "integrity": "sha512-tGZiZ2Gtc4m3wIdLkZ8mkj1T6CEHb35+VApbL2T14Dew8HA7c+04dmKqsKRNC+8RJPm16JEK0tFSwdZqubfc4g==", "dev": true, "license": "MIT", "peerDependencies": { diff --git a/functions/package.json b/functions/package.json index 7c91308..da58b0c 100644 --- a/functions/package.json +++ b/functions/package.json @@ -33,7 +33,7 @@ "devDependencies": { "@faker-js/faker": "^9.6.0", "@types/busboy": "^1.5.4", - "@types/cookie-parser": "^1.4.8", + "@types/cookie-parser": "^1.4.9", "@types/cors": "^2.8.18", "@types/csurf": "^1.11.5", "@types/express": "^5.0.1", diff --git a/functions/src/middlewares/auth_middleware.ts b/functions/src/middlewares/auth_middleware.ts index 46ae735..5432cd9 100644 --- a/functions/src/middlewares/auth_middleware.ts +++ b/functions/src/middlewares/auth_middleware.ts @@ -32,16 +32,13 @@ export const validateSessionCookie = async ( return next(); } - functions.logger.log( - "Checking if request is authorized with session cookies" - ); - const sessionCookie = extractSessionCookieFromCookie(req); // Check for session cookie if (!sessionCookie) { + functions.logger.error("No session cookie found:", req) res.status(401).json({ status: 401, - error: "No session cookie found", + error: "Unauthorized", }); return; } @@ -50,16 +47,12 @@ export const validateSessionCookie = async ( sessionCookie, true ); - functions.logger.log( - "Session cookie correctly decoded", - decodedSessionCookie - ); req.user = decodedSessionCookie; return next(); } catch (error) { functions.logger.error("Error while verifying session cookie:", error); - res.status(401).json({ - status: 401, + res.status(500).json({ + status: 500, error: "Error while verifying session cookie", }); } diff --git a/functions/src/middlewares/role_middleware.ts b/functions/src/middlewares/role_middleware.ts index c4a5b74..0bcf1d0 100644 --- a/functions/src/middlewares/role_middleware.ts +++ b/functions/src/middlewares/role_middleware.ts @@ -1,35 +1,31 @@ -import { NextFunction, Request, Response } from "express"; +import { NextFunction, Response } from "express"; import { auth } from "../config/firebase"; -import { RoleType } from "../models/role"; +import * as functions from "firebase-functions"; -export const restrictToRole = async ( +export const isMentor = async ( req: Request, res: Response, next: NextFunction, - allowedRoles: string[] ) => { try { + // @ts-ignore const sessionCookie = req.cookies.__session; - - // Verify session cookie const decodedClaims = await auth.verifySessionCookie(sessionCookie, true); - - // Check if the user's role is in the allowed roles - const userRole = decodedClaims.role || RoleType.User; - if (!allowedRoles.includes(userRole)) { + const userIsMentor = decodedClaims.mentor === true; + if (!userIsMentor) { return res.status(403).json({ status: 403, error: "Forbidden: Insufficient permissions", }); } - + // @ts-ignore req.user = decodedClaims; return next(); } catch (error) { - console.error("Error verifying session cookie:", error); - return res.status(401).json({ - status: 401, - error: "Unauthorized: Invalid or missing session cookie", + functions.logger.error("Error while verifying user:", error); + return res.status(500).json({ + status: 500, + error: "Something went wrong", }); } -}; +} \ No newline at end of file diff --git a/functions/tsconfig.json b/functions/tsconfig.json index 9705366..c6af828 100644 --- a/functions/tsconfig.json +++ b/functions/tsconfig.json @@ -13,6 +13,7 @@ }, "compileOnSave": true, "include": [ - "src" - ] + "src/**/*" + ], + "exclude": ["node_modules"] } From a986d595f17840aac690d477a957f092d99f6dcf Mon Sep 17 00:00:00 2001 From: Heryan Djaruma Date: Sun, 20 Jul 2025 15:37:44 +0700 Subject: [PATCH 13/40] add my mentorships mentorship --- .../src/controllers/mentorship_controller.ts | 62 ++++++++++++++++++- functions/src/routes/mentorship.ts | 7 ++- 2 files changed, 66 insertions(+), 3 deletions(-) diff --git a/functions/src/controllers/mentorship_controller.ts b/functions/src/controllers/mentorship_controller.ts index 0eee7a8..b136442 100644 --- a/functions/src/controllers/mentorship_controller.ts +++ b/functions/src/controllers/mentorship_controller.ts @@ -3,16 +3,19 @@ import { FirestoreMentor, MentorshipAppointment } from "../models/mentorship"; import { Request, Response } from "express"; import { User } from "../models/user"; import { DateTime } from 'luxon'; -import { MentorshipConfig } from "../types/config"; import { CollectionReference, DocumentData } from "firebase-admin/firestore"; +import { MentorshipConfig } from "../types/config"; +/** + * Get mentorship config. + */ export const getMentorshipConfig = async ( req: Request, res: Response ): Promise => { try { const mentorshipConfigSnapshot = await db.collection("config").doc("mentorshipConfig").get() - const mentorshipConfigData = mentorshipConfigSnapshot.data() + const mentorshipConfigData = mentorshipConfigSnapshot.data() as MentorshipConfig if (!mentorshipConfigSnapshot.exists || mentorshipConfigData === undefined) { res.status(400).json({ @@ -30,6 +33,61 @@ export const getMentorshipConfig = async ( } } + +/******************** + * MENTOR ENDPOINTS * + ********************/ +export const mentorGetMyMentorships = async ( + req: Request, + res: Response +): Promise => { + try { + // 1. Get user + const uid = req.user?.uid! + + // available query params + const { + upcomingOnly, + recentOnly, + isBooked, + isAvailable, + } = req.query; + + // 2. Build Query Dynamically + let query = db.collection("mentorships"); + + query = query.where("mentorId", "==", uid) as CollectionReference; + + const currentTimeSeconds = Math.floor(DateTime.now().setZone('Asia/Jakarta').toUnixInteger()); + if (upcomingOnly) { + query = query.where("startTime", ">=", currentTimeSeconds) as CollectionReference; + } else if (recentOnly) { + query = query.where("startTime", "<=", currentTimeSeconds) as CollectionReference; + } + + if (isBooked) { + query = query.where("hackerId", "!=", null) as CollectionReference; + } else if (isAvailable) { + query = query.where("hackerId", "==", null) as CollectionReference; + } + + // 3. Execute Query and Send Response + const snapshot = await query.orderBy("startTime", "asc").get(); + + const mentorshipAppointments = snapshot.docs.map((doc) => ({ + id: doc.id, + ...doc.data(), + })) as MentorshipAppointment[]; + + res.status(200).json({ + status: 200, + data: mentorshipAppointments, + }); + } catch (error) { + res.status(500).json({ error: (error as Error).message }) + } +} + export const getMentor = async ( req: Request, res: Response diff --git a/functions/src/routes/mentorship.ts b/functions/src/routes/mentorship.ts index 20d558e..fdbc3a6 100644 --- a/functions/src/routes/mentorship.ts +++ b/functions/src/routes/mentorship.ts @@ -1,8 +1,13 @@ import express, { Request, Response } from "express"; -import { bookAMentorshipAppointment, getMentor, getMentors, getMentorshipAppointmentsByMentorId, getMentorshipConfig, getMyMentorshipAppointments } from "../controllers/mentorship_controller"; +import { bookAMentorshipAppointment, getMentor, getMentors, getMentorshipAppointmentsByMentorId, getMentorshipConfig, getMyMentorshipAppointments, mentorGetMyMentorships } from "../controllers/mentorship_controller"; +import { isMentor } from "../middlewares/role_middleware"; const router = express.Router(); +// ****FOR MENTORS ONLY**** +// @ts-ignore +router.get("/mentor/my-mentorships", isMentor, (req: Request, res: Response) => mentorGetMyMentorships(req, res)) + router.get("/config", (req: Request, res: Response) => getMentorshipConfig(req, res)) router.get("/mentors", (req: Request, res: Response) => getMentors(req, res)) router.get("/mentors/:mentorId", (req: Request, res: Response) => getMentor(req, res)) From 8c56709580979808c37dbab44d2085f17701aecf Mon Sep 17 00:00:00 2001 From: Heryan Djaruma Date: Sun, 20 Jul 2025 23:08:22 +0700 Subject: [PATCH 14/40] edit config type --- functions/src/types/config.ts | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/functions/src/types/config.ts b/functions/src/types/config.ts index 0435e04..39614ff 100644 --- a/functions/src/types/config.ts +++ b/functions/src/types/config.ts @@ -1,3 +1,7 @@ +import { Timestamp } from "firebase-admin/firestore"; + export interface MentorshipConfig { - isMentorshipOpen: boolean; + isMentorshipOpen: boolean; // whether or not participant can start to book mentorship slots + mentorshipStartDate: Timestamp; + mentorshipEndDate: Timestamp; } \ No newline at end of file From 3ca63ba98b6a203ce634f3a0e3bba1d52fae24d1 Mon Sep 17 00:00:00 2001 From: Heryan Djaruma Date: Sun, 20 Jul 2025 23:53:17 +0700 Subject: [PATCH 15/40] mentor get my mentorships endpoint --- .../src/controllers/mentorship_controller.ts | 41 +++++++++++-------- functions/src/models/mentorship.ts | 14 +++---- functions/src/models/user.ts | 2 + functions/src/routes/mentorship.ts | 3 +- 4 files changed, 34 insertions(+), 26 deletions(-) diff --git a/functions/src/controllers/mentorship_controller.ts b/functions/src/controllers/mentorship_controller.ts index b136442..6ac08fd 100644 --- a/functions/src/controllers/mentorship_controller.ts +++ b/functions/src/controllers/mentorship_controller.ts @@ -1,10 +1,16 @@ import { db } from "../config/firebase" -import { FirestoreMentor, MentorshipAppointment } from "../models/mentorship"; +import { FirestoreMentor, MentorshipAppointment, MentorshipAppointmentResponseAsHacker, MentorshipAppointmentResponseAsMentor } from "../models/mentorship"; import { Request, Response } from "express"; import { User } from "../models/user"; import { DateTime } from 'luxon'; import { CollectionReference, DocumentData } from "firebase-admin/firestore"; import { MentorshipConfig } from "../types/config"; +import * as functions from "firebase-functions"; + +const MENTORSHIPS = "mentorships"; +const MENTOR_ID = "mentorId"; +const USERS = "users"; +const START_TIME = "startTime"; /** * Get mentorship config. @@ -42,7 +48,6 @@ export const mentorGetMyMentorships = async ( res: Response ): Promise => { try { - // 1. Get user const uid = req.user?.uid! // available query params @@ -53,41 +58,41 @@ export const mentorGetMyMentorships = async ( isAvailable, } = req.query; - // 2. Build Query Dynamically - let query = db.collection("mentorships"); + let query: CollectionReference | DocumentData = db.collection(MENTORSHIPS); - query = query.where("mentorId", "==", uid) as CollectionReference; + query = query.where(MENTOR_ID, "==", uid); const currentTimeSeconds = Math.floor(DateTime.now().setZone('Asia/Jakarta').toUnixInteger()); - if (upcomingOnly) { - query = query.where("startTime", ">=", currentTimeSeconds) as CollectionReference; - } else if (recentOnly) { - query = query.where("startTime", "<=", currentTimeSeconds) as CollectionReference; + if (upcomingOnly === 'true') { + query = query.where(START_TIME, ">=", currentTimeSeconds); + } else if (recentOnly === 'true') { + query = query.where(START_TIME, "<=", currentTimeSeconds); } - if (isBooked) { - query = query.where("hackerId", "!=", null) as CollectionReference; - } else if (isAvailable) { - query = query.where("hackerId", "==", null) as CollectionReference; - } - - // 3. Execute Query and Send Response const snapshot = await query.orderBy("startTime", "asc").get(); - const mentorshipAppointments = snapshot.docs.map((doc) => ({ + let mentorships = snapshot.docs.map((doc: any) => ({ id: doc.id, ...doc.data(), })) as MentorshipAppointment[]; + if (isBooked === 'true') { + mentorships = mentorships.filter(m => m.hackerId != null); + } else if (isAvailable === 'true') { + mentorships = mentorships.filter(m => m.hackerId == null); + } + res.status(200).json({ status: 200, - data: mentorshipAppointments, + data: mentorships, }); } catch (error) { + functions.logger.error(`Error when trying mentorGetMyMentorship: ${(error as Error).message}`) res.status(500).json({ error: (error as Error).message }) } } + export const getMentor = async ( req: Request, res: Response diff --git a/functions/src/models/mentorship.ts b/functions/src/models/mentorship.ts index 0f8d90a..7bb5358 100644 --- a/functions/src/models/mentorship.ts +++ b/functions/src/models/mentorship.ts @@ -30,14 +30,14 @@ export interface MentorshipAppointmentResponseAsMentor { endTime: number; mentorId: string; hackerId?: string; - hackerName: string; - teamName: string; + hackerName?: string; + teamName?: string; hackerDescription?: string; // desc given needed by hacker location: string; // offline or online offlineLocation?: string; // to be filled if the location is offline - mentorMarkAsDone: boolean; - mentorMarkAsAfk: boolean; // mark if this team is AFK - mentorNotes: string // to give this appointment a note + mentorMarkAsDone?: boolean; + mentorMarkAsAfk?: boolean; // mark if this team is AFK + mentorNotes?: string // to give this appointment a note } export interface MentorshipAppointmentResponseAsHacker { @@ -46,8 +46,8 @@ export interface MentorshipAppointmentResponseAsHacker { endTime: number; mentorId: string; hackerId?: string; - hackerName: string; - teamName: string; + hackerName?: string; + teamName?: string; hackerDescription?: string; // desc given needed by hacker location: string; // offline or online offlineLocation?: string; // to be filled if the location is offline diff --git a/functions/src/models/user.ts b/functions/src/models/user.ts index 800ecea..3692710 100644 --- a/functions/src/models/user.ts +++ b/functions/src/models/user.ts @@ -1,6 +1,7 @@ export interface User { id?: string; // linked to uid in firebase doc (not in the field) + displayName: string; firstName: string; lastName: string; email: string; @@ -17,6 +18,7 @@ export interface User { } export const formatUser = (data: Partial): User => ({ + displayName: data.displayName || "", firstName: data.firstName || "", lastName: data.lastName || "", email: data.email || "", diff --git a/functions/src/routes/mentorship.ts b/functions/src/routes/mentorship.ts index fdbc3a6..15d5061 100644 --- a/functions/src/routes/mentorship.ts +++ b/functions/src/routes/mentorship.ts @@ -4,11 +4,12 @@ import { isMentor } from "../middlewares/role_middleware"; const router = express.Router(); +router.get("/config", (req: Request, res: Response) => getMentorshipConfig(req, res)) + // ****FOR MENTORS ONLY**** // @ts-ignore router.get("/mentor/my-mentorships", isMentor, (req: Request, res: Response) => mentorGetMyMentorships(req, res)) -router.get("/config", (req: Request, res: Response) => getMentorshipConfig(req, res)) router.get("/mentors", (req: Request, res: Response) => getMentors(req, res)) router.get("/mentors/:mentorId", (req: Request, res: Response) => getMentor(req, res)) router.get("/mentorships/:mentorId", (req: Request, res: Response) => getMentorshipAppointmentsByMentorId(req, res)) From 3b6c6be86f1afd9ec6a7b8e58ac8ab096ba2ce66 Mon Sep 17 00:00:00 2001 From: Heryan Djaruma Date: Mon, 21 Jul 2025 00:01:01 +0700 Subject: [PATCH 16/40] add mentor Get my mentorship --- .../src/controllers/mentorship_controller.ts | 34 ++++++++++++++++++- functions/src/routes/mentorship.ts | 14 ++++---- 2 files changed, 41 insertions(+), 7 deletions(-) diff --git a/functions/src/controllers/mentorship_controller.ts b/functions/src/controllers/mentorship_controller.ts index 6ac08fd..413cdf5 100644 --- a/functions/src/controllers/mentorship_controller.ts +++ b/functions/src/controllers/mentorship_controller.ts @@ -83,9 +83,41 @@ export const mentorGetMyMentorships = async ( } res.status(200).json({ - status: 200, data: mentorships, }); + } catch (error) { + functions.logger.error(`Error when trying mentorGetMyMentorships: ${(error as Error).message}`) + res.status(500).json({ error: (error as Error).message }) + } +} + +export const mentorGetMyMentorship = async ( + req: Request, + res: Response +): Promise => { + try { + const { id } = req.params + + // 1. Validate id is in param + if (!id) { + res.status(400).json({ + error: "id is required" + }) + return; + } + + // 2. Get a mentroship appointment + const snapshot = await db.collection(MENTORSHIPS).doc(id).get() + if (!snapshot.exists) { + res.status(400).json({ + error: "Cannot find mentorship" + }) + return; + } + + res.status(200).json({ + data: snapshot.data() + }) } catch (error) { functions.logger.error(`Error when trying mentorGetMyMentorship: ${(error as Error).message}`) res.status(500).json({ error: (error as Error).message }) diff --git a/functions/src/routes/mentorship.ts b/functions/src/routes/mentorship.ts index 15d5061..13d2f06 100644 --- a/functions/src/routes/mentorship.ts +++ b/functions/src/routes/mentorship.ts @@ -1,5 +1,5 @@ import express, { Request, Response } from "express"; -import { bookAMentorshipAppointment, getMentor, getMentors, getMentorshipAppointmentsByMentorId, getMentorshipConfig, getMyMentorshipAppointments, mentorGetMyMentorships } from "../controllers/mentorship_controller"; +import { bookAMentorshipAppointment, getMentor, getMentors, getMentorshipAppointmentsByMentorId, getMentorshipConfig, getMyMentorshipAppointments, mentorGetMyMentorship, mentorGetMyMentorships } from "../controllers/mentorship_controller"; import { isMentor } from "../middlewares/role_middleware"; const router = express.Router(); @@ -8,12 +8,14 @@ router.get("/config", (req: Request, res: Response) => getMentorshipConfig(req, // ****FOR MENTORS ONLY**** // @ts-ignore +router.get("/mentor/my-mentorships/:id", isMentor, (req: Request, res: Response) => mentorGetMyMentorship(req, res)) +// @ts-ignore router.get("/mentor/my-mentorships", isMentor, (req: Request, res: Response) => mentorGetMyMentorships(req, res)) -router.get("/mentors", (req: Request, res: Response) => getMentors(req, res)) -router.get("/mentors/:mentorId", (req: Request, res: Response) => getMentor(req, res)) -router.get("/mentorships/:mentorId", (req: Request, res: Response) => getMentorshipAppointmentsByMentorId(req, res)) -router.post("/mentorships", (req: Request, res: Response) => bookAMentorshipAppointment(req, res)) -router.get("/my-mentorships", (req: Request, res: Response) => getMyMentorshipAppointments(req, res)) +// router.get("/mentors", (req: Request, res: Response) => getMentors(req, res)) +// router.get("/mentors/:mentorId", (req: Request, res: Response) => getMentor(req, res)) +// router.get("/mentorships/:mentorId", (req: Request, res: Response) => getMentorshipAppointmentsByMentorId(req, res)) +// router.post("/mentorships", (req: Request, res: Response) => bookAMentorshipAppointment(req, res)) +// router.get("/my-mentorships", (req: Request, res: Response) => getMyMentorshipAppointments(req, res)) export default router; \ No newline at end of file From f23b488c1b10bc9bc1009a0b0ddf641ea887e49f Mon Sep 17 00:00:00 2001 From: Heryan Djaruma Date: Mon, 21 Jul 2025 00:28:16 +0700 Subject: [PATCH 17/40] update firebase function --- functions/package-lock.json | 8 ++++---- functions/package.json | 2 +- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/functions/package-lock.json b/functions/package-lock.json index 69facc0..71eab4a 100644 --- a/functions/package-lock.json +++ b/functions/package-lock.json @@ -13,7 +13,7 @@ "dotenv": "^16.4.7", "express": "^4.21.2", "firebase-admin": "^12.7.0", - "firebase-functions": "^6.3.2", + "firebase-functions": "^6.4.0", "jsonwebtoken": "^9.0.2", "lodash": "^4.17.21", "luxon": "^3.7.1", @@ -4388,9 +4388,9 @@ } }, "node_modules/firebase-functions": { - "version": "6.3.2", - "resolved": "https://registry.npmjs.org/firebase-functions/-/firebase-functions-6.3.2.tgz", - "integrity": "sha512-FC3A1/nhqt1ZzxRnj5HZLScQaozAcFSD/vSR8khqSoFNOfxuXgwJS6ZABTB7+v+iMD5z6Mmxw6OfqITUBuI7OQ==", + "version": "6.4.0", + "resolved": "https://registry.npmjs.org/firebase-functions/-/firebase-functions-6.4.0.tgz", + "integrity": "sha512-Q/LGhJrmJEhT0dbV60J4hCkVSeOM6/r7xJS/ccmkXzTWMjo+UPAYX9zlQmGlEjotstZ0U9GtQSJSgbB2Z+TJDg==", "license": "MIT", "dependencies": { "@types/cors": "^2.8.5", diff --git a/functions/package.json b/functions/package.json index da58b0c..521f680 100644 --- a/functions/package.json +++ b/functions/package.json @@ -23,7 +23,7 @@ "dotenv": "^16.4.7", "express": "^4.21.2", "firebase-admin": "^12.7.0", - "firebase-functions": "^6.3.2", + "firebase-functions": "^6.4.0", "jsonwebtoken": "^9.0.2", "lodash": "^4.17.21", "luxon": "^3.7.1", From 34c546b134b810da2fc37b2ba0063dc6e56bbd4a Mon Sep 17 00:00:00 2001 From: Heryan Djaruma Date: Mon, 21 Jul 2025 00:32:54 +0700 Subject: [PATCH 18/40] mentor update mentorship --- .../src/controllers/mentorship_controller.ts | 52 ++++++++++++++++++- functions/src/routes/mentorship.ts | 4 +- 2 files changed, 54 insertions(+), 2 deletions(-) diff --git a/functions/src/controllers/mentorship_controller.ts b/functions/src/controllers/mentorship_controller.ts index 413cdf5..70eb27e 100644 --- a/functions/src/controllers/mentorship_controller.ts +++ b/functions/src/controllers/mentorship_controller.ts @@ -97,7 +97,7 @@ export const mentorGetMyMentorship = async ( ): Promise => { try { const { id } = req.params - + // 1. Validate id is in param if (!id) { res.status(400).json({ @@ -124,6 +124,56 @@ export const mentorGetMyMentorship = async ( } } +export const mentorPutMyMentorship = async ( + req: Request, + res: Response +) => { + try { + const { id } = req.params + console.log("I WAS HIT") + + // 1. Validate id is in param + if (!id) { + res.status(400).json({ + error: "id is required" + }) + return; + } + + const { + mentorNotes, + mentorMarkAsDone, + mentorMarkAsAfk + } = req.body + + const payload: { [key: string]: any } = {}; + if (mentorNotes !== undefined) { + payload.mentorNotes = mentorNotes; + } + if (mentorMarkAsDone !== undefined) { + payload.mentorMarkAsDone = mentorMarkAsDone; + } + if (mentorMarkAsAfk !== undefined) { + payload.mentorMarkAsAfk = mentorMarkAsAfk; + } + + if (Object.keys(payload).length === 0) { + return res.status(400).json({ error: "No fields to update were provided." }); + } + + db.collection(MENTORSHIPS).doc(id); + + return res.status(200).json({ + message: "Success updated" + }); + } catch (error: any) { + if (error.code === 5) { + return res.status(404).json({ error: "Mentorship with that ID was not found." }); + } + return res.status(500).json({ error: "An unexpected error occurred." }); + } +} + export const getMentor = async ( req: Request, diff --git a/functions/src/routes/mentorship.ts b/functions/src/routes/mentorship.ts index 13d2f06..cb5cea2 100644 --- a/functions/src/routes/mentorship.ts +++ b/functions/src/routes/mentorship.ts @@ -1,5 +1,5 @@ import express, { Request, Response } from "express"; -import { bookAMentorshipAppointment, getMentor, getMentors, getMentorshipAppointmentsByMentorId, getMentorshipConfig, getMyMentorshipAppointments, mentorGetMyMentorship, mentorGetMyMentorships } from "../controllers/mentorship_controller"; +import { bookAMentorshipAppointment, getMentor, getMentors, getMentorshipAppointmentsByMentorId, getMentorshipConfig, getMyMentorshipAppointments, mentorGetMyMentorship, mentorGetMyMentorships, mentorPutMyMentorship } from "../controllers/mentorship_controller"; import { isMentor } from "../middlewares/role_middleware"; const router = express.Router(); @@ -10,6 +10,8 @@ router.get("/config", (req: Request, res: Response) => getMentorshipConfig(req, // @ts-ignore router.get("/mentor/my-mentorships/:id", isMentor, (req: Request, res: Response) => mentorGetMyMentorship(req, res)) // @ts-ignore +router.post("/mentor/my-mentorships/:id", isMentor, (req: Request, res: Response) => mentorPutMyMentorship(req, res)) +// @ts-ignore router.get("/mentor/my-mentorships", isMentor, (req: Request, res: Response) => mentorGetMyMentorships(req, res)) // router.get("/mentors", (req: Request, res: Response) => getMentors(req, res)) From ca6e9117d6593b446d92661c96058127ccb14989 Mon Sep 17 00:00:00 2001 From: Heryan Djaruma Date: Mon, 21 Jul 2025 01:37:58 +0700 Subject: [PATCH 19/40] add get mentors --- .../src/controllers/mentorship_controller.ts | 86 ++++++++++--------- functions/src/routes/mentorship.ts | 24 ++++-- 2 files changed, 65 insertions(+), 45 deletions(-) diff --git a/functions/src/controllers/mentorship_controller.ts b/functions/src/controllers/mentorship_controller.ts index 70eb27e..d7c2f8a 100644 --- a/functions/src/controllers/mentorship_controller.ts +++ b/functions/src/controllers/mentorship_controller.ts @@ -18,24 +18,22 @@ const START_TIME = "startTime"; export const getMentorshipConfig = async ( req: Request, res: Response -): Promise => { +) => { try { const mentorshipConfigSnapshot = await db.collection("config").doc("mentorshipConfig").get() const mentorshipConfigData = mentorshipConfigSnapshot.data() as MentorshipConfig if (!mentorshipConfigSnapshot.exists || mentorshipConfigData === undefined) { - res.status(400).json({ + return res.status(400).json({ status: 400, error: "Config not found" }) - return; } - res.status(200).json({ - status: 200, + return res.status(200).json({ data: mentorshipConfigData }) } catch (error) { - res.status(500).json({ error: (error as Error).message }) + return res.status(500).json({ error: (error as Error).message }) } } @@ -46,7 +44,7 @@ export const getMentorshipConfig = async ( export const mentorGetMyMentorships = async ( req: Request, res: Response -): Promise => { +) => { try { const uid = req.user?.uid! @@ -82,19 +80,19 @@ export const mentorGetMyMentorships = async ( mentorships = mentorships.filter(m => m.hackerId == null); } - res.status(200).json({ + return res.status(200).json({ data: mentorships, }); } catch (error) { functions.logger.error(`Error when trying mentorGetMyMentorships: ${(error as Error).message}`) - res.status(500).json({ error: (error as Error).message }) + return res.status(500).json({ error: (error as Error).message }) } } export const mentorGetMyMentorship = async ( req: Request, res: Response -): Promise => { +) => { try { const { id } = req.params @@ -103,27 +101,33 @@ export const mentorGetMyMentorship = async ( res.status(400).json({ error: "id is required" }) - return; } // 2. Get a mentroship appointment const snapshot = await db.collection(MENTORSHIPS).doc(id).get() if (!snapshot.exists) { - res.status(400).json({ + return res.status(400).json({ error: "Cannot find mentorship" }) - return; } - res.status(200).json({ + return res.status(200).json({ data: snapshot.data() }) } catch (error) { functions.logger.error(`Error when trying mentorGetMyMentorship: ${(error as Error).message}`) - res.status(500).json({ error: (error as Error).message }) + return res.status(500).json({ error: (error as Error).message }) } } +/** + * Updates a mentorship + * @param req.mentorNotes + * @param req.mentorMarkAsDone + * @param req.mentorMarkAsAfk + * @param res + * @returns + */ export const mentorPutMyMentorship = async ( req: Request, res: Response @@ -134,10 +138,9 @@ export const mentorPutMyMentorship = async ( // 1. Validate id is in param if (!id) { - res.status(400).json({ + return res.status(400).json({ error: "id is required" }) - return; } const { @@ -175,6 +178,32 @@ export const mentorPutMyMentorship = async ( } +/******************** + * HACKER ENDPOINTS * + ********************/ +export const hackerGetMentors = async ( + req: Request, + res: Response +) => { + try { + const allMentors: FirestoreMentor[] = []; + const snapshot = await db.collection('users') + .where("mentor", "==", true) + .get() + snapshot.docs.map((mentor) => { + allMentors.push({ + id: mentor.id, + ...mentor.data() + } as FirestoreMentor) + }) + return res.status(200).json({ data: allMentors }) + } catch (error: any) { + functions.logger.error(`Error when trying hackerGetMentors: ${(error as Error).message}`) + return res.status(500).json({ error: "An unexpected error occurred." }); + } +} + + export const getMentor = async ( req: Request, res: Response @@ -232,29 +261,6 @@ export const getMentor = async ( } } -export const getMentors = async ( - req: Request, - res: Response -): Promise => { - const allMentors: FirestoreMentor[] = []; - - try { - const snapshot = await db.collection('users') - .where("mentor", "==", true) - .get() - snapshot.docs.map((mentor) => { - allMentors.push({ - id: mentor.id, - ...mentor.data() - } as FirestoreMentor) - }) - res.status(200).json({ allMentors }) - } catch (error) { - res.status(500).json({ error: (error as Error).message }); - } -} - - /** * Get all mentorship appointments. * diff --git a/functions/src/routes/mentorship.ts b/functions/src/routes/mentorship.ts index cb5cea2..0159a9f 100644 --- a/functions/src/routes/mentorship.ts +++ b/functions/src/routes/mentorship.ts @@ -1,19 +1,33 @@ import express, { Request, Response } from "express"; -import { bookAMentorshipAppointment, getMentor, getMentors, getMentorshipAppointmentsByMentorId, getMentorshipConfig, getMyMentorshipAppointments, mentorGetMyMentorship, mentorGetMyMentorships, mentorPutMyMentorship } from "../controllers/mentorship_controller"; +import { bookAMentorshipAppointment, getMentor, getMentorshipAppointmentsByMentorId, getMentorshipConfig, getMyMentorshipAppointments, hackerGetMentors, mentorGetMyMentorship, mentorGetMyMentorships, mentorPutMyMentorship } from "../controllers/mentorship_controller"; import { isMentor } from "../middlewares/role_middleware"; const router = express.Router(); -router.get("/config", (req: Request, res: Response) => getMentorshipConfig(req, res)) +router.get("/config", async (req: Request, res: Response) => { + await getMentorshipConfig(req, res); +}); // ****FOR MENTORS ONLY**** // @ts-ignore -router.get("/mentor/my-mentorships/:id", isMentor, (req: Request, res: Response) => mentorGetMyMentorship(req, res)) +router.get("/mentor/my-mentorships", isMentor, (req: Request, res: Response) => + mentorGetMyMentorships(req, res) +); + // @ts-ignore -router.post("/mentor/my-mentorships/:id", isMentor, (req: Request, res: Response) => mentorPutMyMentorship(req, res)) +router.get("/mentor/my-mentorships/:id", isMentor, async (req: Request, res: Response) => { + await mentorGetMyMentorship(req, res) +}); + // @ts-ignore -router.get("/mentor/my-mentorships", isMentor, (req: Request, res: Response) => mentorGetMyMentorships(req, res)) +router.post("/mentor/my-mentorships/:id", isMentor, (req: Request, res: Response) => + mentorPutMyMentorship(req, res) +); +// ****FOR HACKERS ONLY**** +router.get("/hackers", async (req: Request, res: Response) => { + await hackerGetMentors(req, res) +}); // router.get("/mentors", (req: Request, res: Response) => getMentors(req, res)) // router.get("/mentors/:mentorId", (req: Request, res: Response) => getMentor(req, res)) // router.get("/mentorships/:mentorId", (req: Request, res: Response) => getMentorshipAppointmentsByMentorId(req, res)) From de8fcaf29817c1be3fac1f235cfbcc45c792fe29 Mon Sep 17 00:00:00 2001 From: Heryan Djaruma Date: Mon, 21 Jul 2025 01:51:52 +0700 Subject: [PATCH 20/40] add limit --- .../src/controllers/mentorship_controller.ts | 15 +++++++++++++-- functions/src/routes/mentorship.ts | 2 +- 2 files changed, 14 insertions(+), 3 deletions(-) diff --git a/functions/src/controllers/mentorship_controller.ts b/functions/src/controllers/mentorship_controller.ts index d7c2f8a..c03d119 100644 --- a/functions/src/controllers/mentorship_controller.ts +++ b/functions/src/controllers/mentorship_controller.ts @@ -186,10 +186,21 @@ export const hackerGetMentors = async ( res: Response ) => { try { + const { limit } = req.query + const allMentors: FirestoreMentor[] = []; - const snapshot = await db.collection('users') + let query = db.collection('users') .where("mentor", "==", true) - .get() + + if (limit) { + const numericLimit = parseInt(limit as string, 10); + if (!isNaN(numericLimit) && numericLimit > 0) { + query = query.limit(numericLimit); + } + } + + const snapshot = await query.get() + snapshot.docs.map((mentor) => { allMentors.push({ id: mentor.id, diff --git a/functions/src/routes/mentorship.ts b/functions/src/routes/mentorship.ts index 0159a9f..fb01ab6 100644 --- a/functions/src/routes/mentorship.ts +++ b/functions/src/routes/mentorship.ts @@ -25,7 +25,7 @@ router.post("/mentor/my-mentorships/:id", isMentor, (req: Request, res: Response ); // ****FOR HACKERS ONLY**** -router.get("/hackers", async (req: Request, res: Response) => { +router.get("/hacker/mentors", async (req: Request, res: Response) => { await hackerGetMentors(req, res) }); // router.get("/mentors", (req: Request, res: Response) => getMentors(req, res)) From 3b5db625642c7edb60ec66e69e8d1849fa91c016 Mon Sep 17 00:00:00 2001 From: Heryan Djaruma Date: Mon, 21 Jul 2025 01:56:38 +0700 Subject: [PATCH 21/40] add get mentor --- .../src/controllers/mentorship_controller.ts | 26 ++++++++++++++++++- functions/src/routes/mentorship.ts | 5 +++- 2 files changed, 29 insertions(+), 2 deletions(-) diff --git a/functions/src/controllers/mentorship_controller.ts b/functions/src/controllers/mentorship_controller.ts index c03d119..29b184f 100644 --- a/functions/src/controllers/mentorship_controller.ts +++ b/functions/src/controllers/mentorship_controller.ts @@ -134,7 +134,6 @@ export const mentorPutMyMentorship = async ( ) => { try { const { id } = req.params - console.log("I WAS HIT") // 1. Validate id is in param if (!id) { @@ -214,6 +213,31 @@ export const hackerGetMentors = async ( } } +export const hackerGetMentor = async ( + req: Request, + res: Response +) => { + try { + const { id } = req.params; + + // 1. Validate id + const snapshot = await db.collection(USERS).doc(id).get() + if (!snapshot.exists) { + return res.status(400).json({ + error: "Cannot find mentor" + }) + } + + const data = snapshot.data() + + return res.status(200).json({data: data}) + } catch (error) { + functions.logger.error(`Error when trying hackerGetMentor: ${(error as Error).message}`) + return res.status(500).json({ error: "An unexpected error occurred." }); + } +} + + export const getMentor = async ( req: Request, diff --git a/functions/src/routes/mentorship.ts b/functions/src/routes/mentorship.ts index fb01ab6..b471419 100644 --- a/functions/src/routes/mentorship.ts +++ b/functions/src/routes/mentorship.ts @@ -1,5 +1,5 @@ import express, { Request, Response } from "express"; -import { bookAMentorshipAppointment, getMentor, getMentorshipAppointmentsByMentorId, getMentorshipConfig, getMyMentorshipAppointments, hackerGetMentors, mentorGetMyMentorship, mentorGetMyMentorships, mentorPutMyMentorship } from "../controllers/mentorship_controller"; +import { bookAMentorshipAppointment, getMentor, getMentorshipAppointmentsByMentorId, getMentorshipConfig, getMyMentorshipAppointments, hackerGetMentor, hackerGetMentors, mentorGetMyMentorship, mentorGetMyMentorships, mentorPutMyMentorship } from "../controllers/mentorship_controller"; import { isMentor } from "../middlewares/role_middleware"; const router = express.Router(); @@ -28,6 +28,9 @@ router.post("/mentor/my-mentorships/:id", isMentor, (req: Request, res: Response router.get("/hacker/mentors", async (req: Request, res: Response) => { await hackerGetMentors(req, res) }); +router.get("/hacker/mentors/:id", async (req: Request, res: Response) => { + await hackerGetMentor(req, res) +}); // router.get("/mentors", (req: Request, res: Response) => getMentors(req, res)) // router.get("/mentors/:mentorId", (req: Request, res: Response) => getMentor(req, res)) // router.get("/mentorships/:mentorId", (req: Request, res: Response) => getMentorshipAppointmentsByMentorId(req, res)) From bf51a912407c69b054e3e94d4a9b176beacbe774 Mon Sep 17 00:00:00 2001 From: Heryan Djaruma Date: Mon, 21 Jul 2025 02:31:21 +0700 Subject: [PATCH 22/40] add concurrently --- functions/package-lock.json | 76 +++++++++++++++++++++++++++++++++++++ functions/package.json | 3 +- 2 files changed, 78 insertions(+), 1 deletion(-) diff --git a/functions/package-lock.json b/functions/package-lock.json index 71eab4a..0192d2c 100644 --- a/functions/package-lock.json +++ b/functions/package-lock.json @@ -34,6 +34,7 @@ "@types/validator": "^13.12.2", "@typescript-eslint/eslint-plugin": "^5.62.0", "@typescript-eslint/parser": "^5.62.0", + "concurrently": "^9.2.0", "eslint": "^8.9.0", "eslint-config-google": "^0.14.0", "eslint-config-prettier": "^10.1.1", @@ -3064,6 +3065,48 @@ "dev": true, "license": "MIT" }, + "node_modules/concurrently": { + "version": "9.2.0", + "resolved": "https://registry.npmjs.org/concurrently/-/concurrently-9.2.0.tgz", + "integrity": "sha512-IsB/fiXTupmagMW4MNp2lx2cdSN2FfZq78vF90LBB+zZHArbIQZjQtzXCiXnvTxCZSvXanTqFLWBjw2UkLx1SQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "chalk": "^4.1.2", + "lodash": "^4.17.21", + "rxjs": "^7.8.1", + "shell-quote": "^1.8.1", + "supports-color": "^8.1.1", + "tree-kill": "^1.2.2", + "yargs": "^17.7.2" + }, + "bin": { + "conc": "dist/bin/concurrently.js", + "concurrently": "dist/bin/concurrently.js" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/open-cli-tools/concurrently?sponsor=1" + } + }, + "node_modules/concurrently/node_modules/supports-color": { + "version": "8.1.1", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-8.1.1.tgz", + "integrity": "sha512-MpUEN2OodtUzxvKQl72cUF7RQ5EiHsGvSsVG0ia9c5RbWGL2CI4C7EpPS8UTBIplnlzZiNuV56w+FuNxy3ty2Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/supports-color?sponsor=1" + } + }, "node_modules/content-disposition": { "version": "0.5.4", "resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-0.5.4.tgz", @@ -7929,6 +7972,16 @@ "queue-microtask": "^1.2.2" } }, + "node_modules/rxjs": { + "version": "7.8.2", + "resolved": "https://registry.npmjs.org/rxjs/-/rxjs-7.8.2.tgz", + "integrity": "sha512-dhKf903U/PQZY6boNNtAGdWbG85WAbjT/1xYoZIC7FAY0yWapOBQVsVrDl58W86//e1VpMNBtRV4MaXfdMySFA==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "tslib": "^2.1.0" + } + }, "node_modules/safe-array-concat": { "version": "1.1.3", "resolved": "https://registry.npmjs.org/safe-array-concat/-/safe-array-concat-1.1.3.tgz", @@ -8175,6 +8228,19 @@ "node": ">=8" } }, + "node_modules/shell-quote": { + "version": "1.8.3", + "resolved": "https://registry.npmjs.org/shell-quote/-/shell-quote-1.8.3.tgz", + "integrity": "sha512-ObmnIF4hXNg1BqhnHmgbDETF8dLPCggZWBjkQfhZpbszZnYur5DUljTcCHii5LC3J5E0yeO/1LIMyH+UvHQgyw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/side-channel": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.1.0.tgz", @@ -8674,6 +8740,16 @@ "license": "MIT", "optional": true }, + "node_modules/tree-kill": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/tree-kill/-/tree-kill-1.2.2.tgz", + "integrity": "sha512-L0Orpi8qGpRG//Nd+H90vFB+3iHnue1zSSGmNOOCh1GLJ7rUKVwV2HvijphGQS2UmhUZewS9VgvxYIdgr+fG1A==", + "dev": true, + "license": "MIT", + "bin": { + "tree-kill": "cli.js" + } + }, "node_modules/ts-deepmerge": { "version": "2.0.7", "resolved": "https://registry.npmjs.org/ts-deepmerge/-/ts-deepmerge-2.0.7.tgz", diff --git a/functions/package.json b/functions/package.json index 521f680..92fad9f 100644 --- a/functions/package.json +++ b/functions/package.json @@ -5,7 +5,7 @@ "lint": "eslint --ext .js,.ts .", "build": "tsc", "build:watch": "tsc --watch --preserveWatchOutput", - "serve": "npm run build:watch | firebase emulators:start", + "serve": "concurrently \"npm:build:watch\" \"firebase emulators:start\"", "shell": "npm run build && firebase functions:shell", "start": "npm run shell", "deploy": "firebase deploy --only functions", @@ -44,6 +44,7 @@ "@types/validator": "^13.12.2", "@typescript-eslint/eslint-plugin": "^5.62.0", "@typescript-eslint/parser": "^5.62.0", + "concurrently": "^9.2.0", "eslint": "^8.9.0", "eslint-config-google": "^0.14.0", "eslint-config-prettier": "^10.1.1", From 31b37784215fcee460cba67cf773282f4cce659f Mon Sep 17 00:00:00 2001 From: Heryan Djaruma Date: Mon, 21 Jul 2025 02:32:01 +0700 Subject: [PATCH 23/40] add get mentorship schedules --- .../src/controllers/mentorship_controller.ts | 40 ++++++++++++++++++- functions/src/routes/mentorship.ts | 7 +++- 2 files changed, 45 insertions(+), 2 deletions(-) diff --git a/functions/src/controllers/mentorship_controller.ts b/functions/src/controllers/mentorship_controller.ts index 29b184f..173706f 100644 --- a/functions/src/controllers/mentorship_controller.ts +++ b/functions/src/controllers/mentorship_controller.ts @@ -230,13 +230,51 @@ export const hackerGetMentor = async ( const data = snapshot.data() - return res.status(200).json({data: data}) + return res.status(200).json({ data: data }) } catch (error) { functions.logger.error(`Error when trying hackerGetMentor: ${(error as Error).message}`) return res.status(500).json({ error: "An unexpected error occurred." }); } } +export const hackerGetMentorSchedules = async ( + req: Request, + res: Response +) => { + try { + const { mentorId, limit } = req.query + + if (!mentorId) { + return res.status(400).json({ error: "mentorId is required as argument" }) + } + + let query = db.collection(MENTORSHIPS) + .where(MENTOR_ID, "==", mentorId) + + if (limit) { + const numericLimit = parseInt(limit as string, 10); + if (!isNaN(numericLimit) && numericLimit > 0) { + query = query.limit(numericLimit); + } + } + + const snapshot = await query.get() + + const allSchedules = snapshot.docs.map((doc) => ({ + id: doc.id, + startTime: doc.data().startTime, + endTime: doc.data().endTime, + mentorId: doc.data().mentorId, + hackerId: doc.data().hackerId, + })) as MentorshipAppointmentResponseAsHacker[]; + + return res.status(200).json({ data: allSchedules }); + } catch (error) { + functions.logger.error(`Error when trying hackerGetMentorSchedules: ${(error as Error).message}`) + return res.status(500).json({ error: "An unexpected error occurred." }); + } +} + export const getMentor = async ( diff --git a/functions/src/routes/mentorship.ts b/functions/src/routes/mentorship.ts index b471419..44e4572 100644 --- a/functions/src/routes/mentorship.ts +++ b/functions/src/routes/mentorship.ts @@ -1,5 +1,5 @@ import express, { Request, Response } from "express"; -import { bookAMentorshipAppointment, getMentor, getMentorshipAppointmentsByMentorId, getMentorshipConfig, getMyMentorshipAppointments, hackerGetMentor, hackerGetMentors, mentorGetMyMentorship, mentorGetMyMentorships, mentorPutMyMentorship } from "../controllers/mentorship_controller"; +import { bookAMentorshipAppointment, getMentor, getMentorshipAppointmentsByMentorId, getMentorshipConfig, getMyMentorshipAppointments, hackerGetMentor, hackerGetMentors, hackerGetMentorSchedules, mentorGetMyMentorship, mentorGetMyMentorships, mentorPutMyMentorship } from "../controllers/mentorship_controller"; import { isMentor } from "../middlewares/role_middleware"; const router = express.Router(); @@ -31,6 +31,11 @@ router.get("/hacker/mentors", async (req: Request, res: Response) => { router.get("/hacker/mentors/:id", async (req: Request, res: Response) => { await hackerGetMentor(req, res) }); +router.get("/hacker/mentorships", async (req: Request, res: Response) => { + await hackerGetMentorSchedules(req, res) +}); + + // router.get("/mentors", (req: Request, res: Response) => getMentors(req, res)) // router.get("/mentors/:mentorId", (req: Request, res: Response) => getMentor(req, res)) // router.get("/mentorships/:mentorId", (req: Request, res: Response) => getMentorshipAppointmentsByMentorId(req, res)) From 28e1747fa0a70f8b7d0cc1c008b064ea547c8ac3 Mon Sep 17 00:00:00 2001 From: Heryan Djaruma Date: Mon, 21 Jul 2025 05:19:40 +0700 Subject: [PATCH 24/40] add get mentorship schedule --- .../src/controllers/mentorship_controller.ts | 587 ++++++++++-------- functions/src/data/dummy/user.ts | 1 - functions/src/middlewares/role_middleware.ts | 2 - functions/src/routes/mentorship.ts | 34 +- 4 files changed, 329 insertions(+), 295 deletions(-) diff --git a/functions/src/controllers/mentorship_controller.ts b/functions/src/controllers/mentorship_controller.ts index 173706f..3daefc9 100644 --- a/functions/src/controllers/mentorship_controller.ts +++ b/functions/src/controllers/mentorship_controller.ts @@ -1,5 +1,5 @@ import { db } from "../config/firebase" -import { FirestoreMentor, MentorshipAppointment, MentorshipAppointmentResponseAsHacker, MentorshipAppointmentResponseAsMentor } from "../models/mentorship"; +import { FirestoreMentor, MentorshipAppointment, MentorshipAppointmentResponseAsHacker } from "../models/mentorship"; import { Request, Response } from "express"; import { User } from "../models/user"; import { DateTime } from 'luxon'; @@ -38,7 +38,7 @@ export const getMentorshipConfig = async ( } -/******************** +/** ****************** * MENTOR ENDPOINTS * ********************/ export const mentorGetMyMentorships = async ( @@ -46,7 +46,10 @@ export const mentorGetMyMentorships = async ( res: Response ) => { try { - const uid = req.user?.uid! + const uid = req.user?.uid + if (!uid) { + return res.status(401).send('Unauthorized: User ID not found.'); + } // available query params const { @@ -177,7 +180,7 @@ export const mentorPutMyMentorship = async ( } -/******************** +/** ****************** * HACKER ENDPOINTS * ********************/ export const hackerGetMentors = async ( @@ -275,290 +278,324 @@ export const hackerGetMentorSchedules = async ( } } - - -export const getMentor = async ( - req: Request, - res: Response -): Promise => { - const { mentorId } = req.params; - try { - /** - * Validate request: - * 1. Mentor id is not in params - */ - if (!mentorId) { - res.status(400).json({ - status: 400, - error: "Mentor id is required" - }) - return; - } - - /** - * Process request - * 1. Find mentor in db - */ - const mentorDoc = await db.collection("users").doc(mentorId).get() - if (!mentorDoc.exists) { - res.status(404).json({ - status: 404, - error: "Cannot find mentor with the given mentor id" - }) - return; - } - const mentorData = mentorDoc.data() - - if (mentorData) { - const trimmedData: FirestoreMentor = { - "id": mentorData.id, - "email": mentorData.email, - "name": mentorData.name, - "intro": mentorData.intro, - "specialization": mentorData.specialization, - "mentor": mentorData.mentor, - "discordUsername": mentorData.discordUsername - } - res.status(200).json({ - status: 200, - data: trimmedData - }) - } else { - res.status(200).json({ - status: 200, - data: {} - }) - } - } catch (error) { - res.status(500).json({ error: (error as Error).message }) - } -} - -/** - * Get all mentorship appointments. - * - * To be used in admin. - * @param req - * @param res - */ -export const getMentorshipAppointments = async ( - req: Request, - res: Response -): Promise => { - const mentorshipAppointments: MentorshipAppointment[] = []; - - try { - const snapshot = await db.collection('mentorships') - .where("mentor", "==", true) - .get() - snapshot.docs.map((mentor) => { - mentorshipAppointments.push({ - id: mentor.id, - ...mentor.data() - } as MentorshipAppointment) - }) - res.status(200).json({ mentorshipAppointments }) - } catch (error) { - res.status(500).json({ error: (error as Error).message }); - } -} - -/** - * Get mentorship appointments by mentor id. - * - * Use for: - * 1. Admin - * 2. Mentor in portal - * 3. Hacker in portal - * @param req - * @param res - * @returns - */ -export const getMentorshipAppointmentsByMentorId = async ( +export const hackerGetMentorSchedule = async ( req: Request, res: Response -): Promise => { - try { - const { mentorId } = req.params; - const doc = await db.collection("mentorships") - .where("mentorId", "==", mentorId) - .get(); - - if (doc.empty) { - res.status(404).json({ error: "Cannot find mentorships related with the mentor." }); - return; - } - - const mentorships: MentorshipAppointment[] = []; - doc.docs.map((mentorship) => { - mentorships.push({ - id: mentorship.id, - ...mentorship.data() - } as MentorshipAppointment) - }) - - res.status(200).json(mentorships); - } catch (error) { - res.status(500).json({ error: (error as Error).message }); - } -}; - -/** - * Book mentorship appointment. Use request cookie to get hacker uid. - * - * Use for: - * 1. Hacker in portal - * @param req - * @param res - * @returns - */ -export const bookAMentorshipAppointment = async ( - req: Request, - res: Response -): Promise => { +) => { try { - /** - * Validate current user: - * 1. User is not valid / not authenticated. - * 2. Get current user from db - */ - const uid = req.user?.uid; - if (!uid || uid === undefined) { - res.status(401).json({ - status: 401, - error: "Cannot get current user uid" - }) - return; - } - - const { mentorshipAppointmentId, hackerDescription } = req.body - - // reject if no mentorshipAppointmentId - if (mentorshipAppointmentId === undefined || !mentorshipAppointmentId) { - res.status(400).json({ - status: 400, - error: "mentorshipAppointmentId is required in body" - }) - return - } + const { id } = req.params - /** - * Validation: - * 1. Reject if the mentorship appointment does not exist in db - * 2. Reject if the mentorship is already booked - */ - const mentorshipAppointmentDoc = await db.collection("mentorships").doc(mentorshipAppointmentId) - const mentorshipAppointmentSnap = await mentorshipAppointmentDoc.get() - if (!mentorshipAppointmentSnap.exists) { - res.status(400).json({ - status: 400, - error: "Mentorship appointment does not exist" + // 1. Validate id is in param + if (!id) { + return res.status(400).json({ + error: "id is required" }) - return } - const mentorshipData: MentorshipAppointment | undefined = mentorshipAppointmentSnap.data() as MentorshipAppointment - if (mentorshipData.hackerId) { - res.status(400).json({ - status: 400, - error: "Mentorship slot is already booked" - }) - return + const snapshot = await db.collection(MENTORSHIPS).doc(id).get() + const data = snapshot.data() + if (!snapshot.exists || !data) { + return res.status(404).json({error: "Cannot find mentorship"}) } - /** - * Book the mentorship slot - * 1. Update the hackerId for the document - */ - const updatedMentorshipAppointment = { - hackerId: uid, - hackerDescription: hackerDescription, - ...mentorshipData + const mentorshipAppointmentResponseAsHacker: MentorshipAppointmentResponseAsHacker = { + id: data.id, + startTime: data.startTime, + endTime: data.endTime, + mentorId: data.mentorId, + hackerId: data.hackerId } - await mentorshipAppointmentDoc.update(updatedMentorshipAppointment) - res.status(200).json({ - status: 200, - data: "Successfuly booked mentorship slot" - }) + return res.status(200).json({data: mentorshipAppointmentResponseAsHacker}) } catch (error) { - res.status(500).json({ error: (error as Error).message }) + functions.logger.error(`Error when trying hackerGetMentorSchedules: ${(error as Error).message}`) + return res.status(500).json({ error: "An unexpected error occurred." }); } } -/** - * Get the list of my mentorship appointments. - * If user is mentor, by default will return his appointment. - * Use request cookie to get uid. - * - * Use for: - * 1. Mentor in portal - * 2. Hacker in portal - * - * @param req.query.upcomingOnly boolean : If or not only fetches upcoming only. - * @param req.query.recentOnly boolean : If or not only fetches recent only. - */ -export const getMyMentorshipAppointments = async ( - req: Request, - res: Response -): Promise => { - try { - // 1. Validate User - const uid = req.user?.uid; - if (!uid) { - res.status(401).json({ status: 401, error: "Cannot get current user uid" }); - return; - } - - const currentUserSnap = await db.collection("users").doc(uid).get(); - if (!currentUserSnap.exists) { - res.status(400).json({ status: 400, error: "User not found" }); - return; - } - - const currentUserData = currentUserSnap.data() as FirestoreMentor | User; - const upcomingOnly = req.query.upcomingOnly === 'true'; - const recentOnly = req.query.recentOnly === 'true'; - - // 2. Build Query Dynamically - let query = db.collection("mentorships"); - - if (isMentor(currentUserData)) { - query = query.where("mentorId", "==", uid) as CollectionReference; - } else { - query = query.where("hackerId", "==", uid) as CollectionReference;; - } - - const currentTimeSeconds = Math.floor(DateTime.now().setZone('Asia/Jakarta').toUnixInteger()); - if (upcomingOnly) { - query = query.where("startTime", ">=", currentTimeSeconds) as CollectionReference; - } else if (recentOnly) { - query = query.where("startTime", "<=", currentTimeSeconds) as CollectionReference; - } - - // 3. Execute Query and Send Response - const snapshot = await query.orderBy("startTime", "asc").get(); - - const mentorshipAppointments = snapshot.docs.map((doc) => ({ - id: doc.id, - ...doc.data(), - })) as MentorshipAppointment[]; - - res.status(200).json({ - status: 200, - data: mentorshipAppointments, - }); - } catch (error) { - res.status(500).json({ error: (error as Error).message }); - } -}; - -/** - * - * @param data - * @returns - */ -function isMentor(data: FirestoreMentor | User): data is FirestoreMentor { - return 'mentor' in data; -} \ No newline at end of file +// export const getMentor = async ( +// req: Request, +// res: Response +// ): Promise => { +// const { mentorId } = req.params; +// try { +// /** +// * Validate request: +// * 1. Mentor id is not in params +// */ +// if (!mentorId) { +// res.status(400).json({ +// status: 400, +// error: "Mentor id is required" +// }) +// return; +// } + +// /** +// * Process request +// * 1. Find mentor in db +// */ +// const mentorDoc = await db.collection("users").doc(mentorId).get() +// if (!mentorDoc.exists) { +// res.status(404).json({ +// status: 404, +// error: "Cannot find mentor with the given mentor id" +// }) +// return; +// } +// const mentorData = mentorDoc.data() + +// if (mentorData) { +// const trimmedData: FirestoreMentor = { +// "id": mentorData.id, +// "email": mentorData.email, +// "name": mentorData.name, +// "intro": mentorData.intro, +// "specialization": mentorData.specialization, +// "mentor": mentorData.mentor, +// "discordUsername": mentorData.discordUsername +// } +// res.status(200).json({ +// status: 200, +// data: trimmedData +// }) +// } else { +// res.status(200).json({ +// status: 200, +// data: {} +// }) +// } +// } catch (error) { +// res.status(500).json({ error: (error as Error).message }) +// } +// } + +// /** +// * Get all mentorship appointments. +// * +// * To be used in admin. +// * @param req +// * @param res +// */ +// export const getMentorshipAppointments = async ( +// req: Request, +// res: Response +// ): Promise => { +// const mentorshipAppointments: MentorshipAppointment[] = []; + +// try { +// const snapshot = await db.collection('mentorships') +// .where("mentor", "==", true) +// .get() +// snapshot.docs.map((mentor) => { +// mentorshipAppointments.push({ +// id: mentor.id, +// ...mentor.data() +// } as MentorshipAppointment) +// }) +// res.status(200).json({ mentorshipAppointments }) +// } catch (error) { +// res.status(500).json({ error: (error as Error).message }); +// } +// } + +// /** +// * Get mentorship appointments by mentor id. +// * +// * Use for: +// * 1. Admin +// * 2. Mentor in portal +// * 3. Hacker in portal +// * @param req +// * @param res +// * @returns +// */ +// export const getMentorshipAppointmentsByMentorId = async ( +// req: Request, +// res: Response +// ): Promise => { +// try { +// const { mentorId } = req.params; +// const doc = await db.collection("mentorships") +// .where("mentorId", "==", mentorId) +// .get(); + +// if (doc.empty) { +// res.status(404).json({ error: "Cannot find mentorships related with the mentor." }); +// return; +// } + +// const mentorships: MentorshipAppointment[] = []; +// doc.docs.map((mentorship) => { +// mentorships.push({ +// id: mentorship.id, +// ...mentorship.data() +// } as MentorshipAppointment) +// }) + +// res.status(200).json(mentorships); +// } catch (error) { +// res.status(500).json({ error: (error as Error).message }); +// } +// }; + +// /** +// * Book mentorship appointment. Use request cookie to get hacker uid. +// * +// * Use for: +// * 1. Hacker in portal +// * @param req +// * @param res +// * @returns +// */ +// export const bookAMentorshipAppointment = async ( +// req: Request, +// res: Response +// ): Promise => { +// try { +// /** +// * Validate current user: +// * 1. User is not valid / not authenticated. +// * 2. Get current user from db +// */ +// const uid = req.user?.uid; +// if (!uid || uid === undefined) { +// res.status(401).json({ +// status: 401, +// error: "Cannot get current user uid" +// }) +// return; +// } + +// const { mentorshipAppointmentId, hackerDescription } = req.body + +// // reject if no mentorshipAppointmentId +// if (mentorshipAppointmentId === undefined || !mentorshipAppointmentId) { +// res.status(400).json({ +// status: 400, +// error: "mentorshipAppointmentId is required in body" +// }) +// return +// } + +// /** +// * Validation: +// * 1. Reject if the mentorship appointment does not exist in db +// * 2. Reject if the mentorship is already booked +// */ +// const mentorshipAppointmentDoc = await db.collection("mentorships").doc(mentorshipAppointmentId) +// const mentorshipAppointmentSnap = await mentorshipAppointmentDoc.get() +// if (!mentorshipAppointmentSnap.exists) { +// res.status(400).json({ +// status: 400, +// error: "Mentorship appointment does not exist" +// }) +// return +// } + +// const mentorshipData: MentorshipAppointment | undefined = mentorshipAppointmentSnap.data() as MentorshipAppointment +// if (mentorshipData.hackerId) { +// res.status(400).json({ +// status: 400, +// error: "Mentorship slot is already booked" +// }) +// return +// } + +// /** +// * Book the mentorship slot +// * 1. Update the hackerId for the document +// */ +// const updatedMentorshipAppointment = { +// hackerId: uid, +// hackerDescription: hackerDescription, +// ...mentorshipData +// } +// await mentorshipAppointmentDoc.update(updatedMentorshipAppointment) +// res.status(200).json({ +// status: 200, +// data: "Successfuly booked mentorship slot" +// }) +// } catch (error) { +// res.status(500).json({ error: (error as Error).message }) +// } +// } + + +// /** +// * Get the list of my mentorship appointments. +// * If user is mentor, by default will return his appointment. +// * Use request cookie to get uid. +// * +// * Use for: +// * 1. Mentor in portal +// * 2. Hacker in portal +// * +// * @param req.query.upcomingOnly boolean : If or not only fetches upcoming only. +// * @param req.query.recentOnly boolean : If or not only fetches recent only. +// */ + +// export const getMyMentorshipAppointments = async ( +// req: Request, +// res: Response +// ): Promise => { +// try { +// // 1. Validate User +// const uid = req.user?.uid; +// if (!uid) { +// res.status(401).json({ status: 401, error: "Cannot get current user uid" }); +// return; +// } + +// const currentUserSnap = await db.collection("users").doc(uid).get(); +// if (!currentUserSnap.exists) { +// res.status(400).json({ status: 400, error: "User not found" }); +// return; +// } + +// const currentUserData = currentUserSnap.data() as FirestoreMentor | User; +// const upcomingOnly = req.query.upcomingOnly === 'true'; +// const recentOnly = req.query.recentOnly === 'true'; + +// // 2. Build Query Dynamically +// let query = db.collection("mentorships"); + +// if (isMentor(currentUserData)) { +// query = query.where("mentorId", "==", uid) as CollectionReference; +// } else { +// query = query.where("hackerId", "==", uid) as CollectionReference;; +// } + +// const currentTimeSeconds = Math.floor(DateTime.now().setZone('Asia/Jakarta').toUnixInteger()); +// if (upcomingOnly) { +// query = query.where("startTime", ">=", currentTimeSeconds) as CollectionReference; +// } else if (recentOnly) { +// query = query.where("startTime", "<=", currentTimeSeconds) as CollectionReference; +// } + +// // 3. Execute Query and Send Response +// const snapshot = await query.orderBy("startTime", "asc").get(); + +// const mentorshipAppointments = snapshot.docs.map((doc) => ({ +// id: doc.id, +// ...doc.data(), +// })) as MentorshipAppointment[]; + +// res.status(200).json({ +// status: 200, +// data: mentorshipAppointments, +// }); +// } catch (error) { +// res.status(500).json({ error: (error as Error).message }); +// } +// }; + +// /** +// * +// * @param data +// * @returns +// */ +// function isMentor(data: FirestoreMentor | User): data is FirestoreMentor { +// return 'mentor' in data; +// } \ No newline at end of file diff --git a/functions/src/data/dummy/user.ts b/functions/src/data/dummy/user.ts index b457c7f..f4720a4 100644 --- a/functions/src/data/dummy/user.ts +++ b/functions/src/data/dummy/user.ts @@ -1,5 +1,4 @@ import { FirestoreMentor } from "../../models/mentorship"; -import { User } from "../../models/user"; export const dummyMentors: FirestoreMentor[] = [ { diff --git a/functions/src/middlewares/role_middleware.ts b/functions/src/middlewares/role_middleware.ts index 0bcf1d0..f978d82 100644 --- a/functions/src/middlewares/role_middleware.ts +++ b/functions/src/middlewares/role_middleware.ts @@ -8,7 +8,6 @@ export const isMentor = async ( next: NextFunction, ) => { try { - // @ts-ignore const sessionCookie = req.cookies.__session; const decodedClaims = await auth.verifySessionCookie(sessionCookie, true); const userIsMentor = decodedClaims.mentor === true; @@ -18,7 +17,6 @@ export const isMentor = async ( error: "Forbidden: Insufficient permissions", }); } - // @ts-ignore req.user = decodedClaims; return next(); } catch (error) { diff --git a/functions/src/routes/mentorship.ts b/functions/src/routes/mentorship.ts index 44e4572..95d818a 100644 --- a/functions/src/routes/mentorship.ts +++ b/functions/src/routes/mentorship.ts @@ -1,38 +1,38 @@ import express, { Request, Response } from "express"; -import { bookAMentorshipAppointment, getMentor, getMentorshipAppointmentsByMentorId, getMentorshipConfig, getMyMentorshipAppointments, hackerGetMentor, hackerGetMentors, hackerGetMentorSchedules, mentorGetMyMentorship, mentorGetMyMentorships, mentorPutMyMentorship } from "../controllers/mentorship_controller"; +import { getMentorshipConfig, hackerGetMentor, hackerGetMentors, hackerGetMentorSchedule, hackerGetMentorSchedules, mentorGetMyMentorship, mentorGetMyMentorships, mentorPutMyMentorship } from "../controllers/mentorship_controller"; import { isMentor } from "../middlewares/role_middleware"; const router = express.Router(); router.get("/config", async (req: Request, res: Response) => { - await getMentorshipConfig(req, res); + await getMentorshipConfig(req, res); }); // ****FOR MENTORS ONLY**** -// @ts-ignore -router.get("/mentor/my-mentorships", isMentor, (req: Request, res: Response) => - mentorGetMyMentorships(req, res) +router.get("/mentor/my-mentorships", isMentor, (req: Request, res: Response) => + mentorGetMyMentorships(req, res) ); -// @ts-ignore -router.get("/mentor/my-mentorships/:id", isMentor, async (req: Request, res: Response) => { - await mentorGetMyMentorship(req, res) +router.get("/mentor/my-mentorships/:id", isMentor, async (req: Request, res: Response) => { + await mentorGetMyMentorship(req, res) }); -// @ts-ignore -router.post("/mentor/my-mentorships/:id", isMentor, (req: Request, res: Response) => - mentorPutMyMentorship(req, res) +router.post("/mentor/my-mentorships/:id", isMentor, (req: Request, res: Response) => + mentorPutMyMentorship(req, res) ); // ****FOR HACKERS ONLY**** -router.get("/hacker/mentors", async (req: Request, res: Response) => { - await hackerGetMentors(req, res) +router.get("/hacker/mentors", async (req: Request, res: Response) => { + await hackerGetMentors(req, res) }); -router.get("/hacker/mentors/:id", async (req: Request, res: Response) => { - await hackerGetMentor(req, res) +router.get("/hacker/mentors/:id", async (req: Request, res: Response) => { + await hackerGetMentor(req, res) }); -router.get("/hacker/mentorships", async (req: Request, res: Response) => { - await hackerGetMentorSchedules(req, res) +router.get("/hacker/mentorships", async (req: Request, res: Response) => { + await hackerGetMentorSchedules(req, res) +}); +router.get("/hacker/mentorships/:id", async (req: Request, res: Response) => { + await hackerGetMentorSchedule(req, res) }); From 4437c7d6872af50071bfb17d5bd3336bdf4d6e80 Mon Sep 17 00:00:00 2001 From: Heryan Djaruma Date: Mon, 21 Jul 2025 06:10:56 +0700 Subject: [PATCH 25/40] add book mentorship endponts --- .../src/controllers/mentorship_controller.ts | 109 +++++++++++++++++- functions/src/routes/mentorship.ts | 5 +- 2 files changed, 109 insertions(+), 5 deletions(-) diff --git a/functions/src/controllers/mentorship_controller.ts b/functions/src/controllers/mentorship_controller.ts index 3daefc9..ba2a608 100644 --- a/functions/src/controllers/mentorship_controller.ts +++ b/functions/src/controllers/mentorship_controller.ts @@ -3,12 +3,13 @@ import { FirestoreMentor, MentorshipAppointment, MentorshipAppointmentResponseAs import { Request, Response } from "express"; import { User } from "../models/user"; import { DateTime } from 'luxon'; -import { CollectionReference, DocumentData } from "firebase-admin/firestore"; +import { CollectionReference, DocumentData, FieldPath } from "firebase-admin/firestore"; import { MentorshipConfig } from "../types/config"; import * as functions from "firebase-functions"; const MENTORSHIPS = "mentorships"; const MENTOR_ID = "mentorId"; +const HACKER_ID = "hackerId"; const USERS = "users"; const START_TIME = "startTime"; @@ -295,7 +296,7 @@ export const hackerGetMentorSchedule = async ( const snapshot = await db.collection(MENTORSHIPS).doc(id).get() const data = snapshot.data() if (!snapshot.exists || !data) { - return res.status(404).json({error: "Cannot find mentorship"}) + return res.status(404).json({ error: "Cannot find mentorship" }) } const mentorshipAppointmentResponseAsHacker: MentorshipAppointmentResponseAsHacker = { @@ -303,15 +304,115 @@ export const hackerGetMentorSchedule = async ( startTime: data.startTime, endTime: data.endTime, mentorId: data.mentorId, - hackerId: data.hackerId + hackerId: data.hackerId, + location: data.location, } - return res.status(200).json({data: mentorshipAppointmentResponseAsHacker}) + return res.status(200).json({ data: mentorshipAppointmentResponseAsHacker }) } catch (error) { functions.logger.error(`Error when trying hackerGetMentorSchedules: ${(error as Error).message}`) return res.status(500).json({ error: "An unexpected error occurred." }); } } +interface BookMentorshipRequest { + id: string; + hackerId: string; + hackerName: string; + teamName: string; + hackerDescription: string; + offlineLocation?: string; +} + +export const hackerBookMentorships = async ( + req: Request, + res: Response +) => { + const MAX_CONCURRENT_BOOKINGS = 2 + try { + const { mentorships }: { mentorships: BookMentorshipRequest[] } = req.body; + + if (!mentorships || !Array.isArray(mentorships) || mentorships.length === 0) { + return res.status(400).json({ error: 'Mentorships must be a non-empty array.' }); + } + + if (mentorships.length > MAX_CONCURRENT_BOOKINGS) { + return res.status(400).json({ error: `Cannot book more than ${MAX_CONCURRENT_BOOKINGS} slots at a time.` }); + } + + const hackerId = mentorships[0].hackerId; + for (const mentorship of mentorships) { + if (!mentorship.id || !mentorship.hackerId || !mentorship.hackerName || !mentorship.teamName || !mentorship.hackerDescription) { + return res.status(400).json({ error: 'Each mentorship must include id, hackerId, hackerName, teamName, and hackerDescription.' }); + } + if (mentorship.hackerId !== hackerId) { + return res.status(400).json({ error: 'All mentorship bookings in a single request must be for the same hacker.' }); + } + } + + const mentorshipsCollection = db.collection(MENTORSHIPS); + const currentTimeSeconds = Math.floor(DateTime.now().setZone('Asia/Jakarta').toUnixInteger()); + const existingBookingsQuery = mentorshipsCollection + .where(HACKER_ID, '==', hackerId) + .where(START_TIME, '>', currentTimeSeconds); + + const existingBookingsSnapshot = await existingBookingsQuery.get(); + + if (existingBookingsSnapshot.size + mentorships.length > MAX_CONCURRENT_BOOKINGS) { + return res.status(400).json({ error: `This request would exceed the maximum of ${MAX_CONCURRENT_BOOKINGS} active bookings.` }); + } + + await db.runTransaction(async (transaction) => { + const mentorshipIds = mentorships.map((m) => m.id); + + const requestedMentorshipsRef = mentorshipsCollection.where(FieldPath.documentId(), 'in', mentorshipIds); + const requestedMentorshipsSnapshot = await transaction.get(requestedMentorshipsRef); + + if (requestedMentorshipsSnapshot.size !== mentorships.length) { + throw new Error("One or more mentorship slots could not be found."); + } + + const thirtyMinsFromNow = Math.floor(DateTime.now().setZone('Asia/Jakarta').toUnixInteger()) + (30 * 60); + + for (const doc of requestedMentorshipsSnapshot.docs) { + const data = doc.data(); + + if (data.hackerId) { + throw new Error(`Mentorship slot ${doc.id} is already booked.`); + } + + if (data.startTime < thirtyMinsFromNow) { + throw new Error(`Mentorship slot ${doc.id} is starting too soon to book.`); + } + } + + for (const mentorshipRequest of mentorships) { + const docRef = mentorshipsCollection.doc(mentorshipRequest.id); + transaction.update(docRef, { + hackerId: mentorshipRequest.hackerId, + hackerName: mentorshipRequest.hackerName, + teamName: mentorshipRequest.teamName, + hackerDescription: mentorshipRequest.hackerDescription, + offlineLocation: mentorshipRequest.offlineLocation || null, + }); + } + }); + + return res.status(200).json({ success: true, message: 'Mentorships booked successfully.' }); + } catch (error) { + const err = error as Error + + if (err.message.includes('mentorship slots could not be found')) { + return res.status(400).json({error: "Mentorship slot(s) could not be found"}) + } else if (err.message.includes('already booked')) { + return res.status(400).json({error: "Mentorship slot(s) are already booked"}) + } else if (err.message.includes('too soon to book')) { + return res.status(400).json({error: "Cannot book less than 30 mins before the mentoring schedule. Please choose another mentorship slot!"}) + } + + functions.logger.error(`Error when trying hackerBookMentorships: ${(error as Error).message}`) + return res.status(500).json({ error: "An unexpected error occurred." }); + } +} // export const getMentor = async ( diff --git a/functions/src/routes/mentorship.ts b/functions/src/routes/mentorship.ts index 95d818a..76f3f0e 100644 --- a/functions/src/routes/mentorship.ts +++ b/functions/src/routes/mentorship.ts @@ -1,5 +1,5 @@ import express, { Request, Response } from "express"; -import { getMentorshipConfig, hackerGetMentor, hackerGetMentors, hackerGetMentorSchedule, hackerGetMentorSchedules, mentorGetMyMentorship, mentorGetMyMentorships, mentorPutMyMentorship } from "../controllers/mentorship_controller"; +import { getMentorshipConfig, hackerBookMentorships, hackerGetMentor, hackerGetMentors, hackerGetMentorSchedule, hackerGetMentorSchedules, mentorGetMyMentorship, mentorGetMyMentorships, mentorPutMyMentorship } from "../controllers/mentorship_controller"; import { isMentor } from "../middlewares/role_middleware"; const router = express.Router(); @@ -34,6 +34,9 @@ router.get("/hacker/mentorships", async (req: Request, res: Response) => { router.get("/hacker/mentorships/:id", async (req: Request, res: Response) => { await hackerGetMentorSchedule(req, res) }); +router.post("/hacker/mentorships/book", async (req: Request, res: Response) => { + await hackerBookMentorships(req, res) +}) // router.get("/mentors", (req: Request, res: Response) => getMentors(req, res)) From a8e0fe977a4e2d2a24ea3f524d8e4a73b3cb2058 Mon Sep 17 00:00:00 2001 From: Heryan Djaruma Date: Mon, 21 Jul 2025 06:34:00 +0700 Subject: [PATCH 26/40] add cancel mentorship endpoint --- .../src/controllers/mentorship_controller.ts | 50 ++++++++++++++++++- functions/src/routes/mentorship.ts | 5 +- 2 files changed, 52 insertions(+), 3 deletions(-) diff --git a/functions/src/controllers/mentorship_controller.ts b/functions/src/controllers/mentorship_controller.ts index ba2a608..178e10f 100644 --- a/functions/src/controllers/mentorship_controller.ts +++ b/functions/src/controllers/mentorship_controller.ts @@ -3,7 +3,7 @@ import { FirestoreMentor, MentorshipAppointment, MentorshipAppointmentResponseAs import { Request, Response } from "express"; import { User } from "../models/user"; import { DateTime } from 'luxon'; -import { CollectionReference, DocumentData, FieldPath } from "firebase-admin/firestore"; +import { CollectionReference, DocumentData, FieldPath, FieldValue } from "firebase-admin/firestore"; import { MentorshipConfig } from "../types/config"; import * as functions from "firebase-functions"; @@ -167,7 +167,11 @@ export const mentorPutMyMentorship = async ( return res.status(400).json({ error: "No fields to update were provided." }); } - db.collection(MENTORSHIPS).doc(id); + await db.collection(MENTORSHIPS).doc(id).update({ + mentorNotes: mentorNotes, + mentorMarkAsDone: mentorMarkAsDone, + mentorMarkAsAfk: mentorMarkAsAfk + }) return res.status(200).json({ message: "Success updated" @@ -414,6 +418,48 @@ export const hackerBookMentorships = async ( } } +export const hackerCancelMentorship = async ( + req: Request, res: Response +) => { + try { + const uid = req.user?.uid + if (!uid) { + return res.status(401).json({error: "Unauthorized"}) + } + + const { id } = req.body; + if (!id) { + return res.status(400).json({error: "id must be in the argument"}) + } + + // Validate + // 1. If mentorship does not exist + // 2. If mentorship does not belong to the hacker + const mentorshipSnapshot = await db.collection(MENTORSHIPS).doc(id).get() + const mentorshipData = mentorshipSnapshot.data() + if (!mentorshipSnapshot.exists || !mentorshipData) { + return res.status(404).json({error: "Cannot find mentorship with the given id"}) + } + + if (mentorshipData.hackerId !== uid) { + return res.status(401).json({error: "Unauthorized"}) + } + + await db.collection(MENTORSHIPS).doc(id).update({ + hackerId: FieldValue.delete(), + hackerName: FieldValue.delete(), + teamName: FieldValue.delete(), + hackerDescription: FieldValue.delete(), + offlineLocation: FieldValue.delete(), + }) + + res.status(200).json({message: "Mentorship has been canceled."}) + } catch (error) { + functions.logger.error(`Error when trying hackerCancelMentorship: ${(error as Error).message}`) + return res.status(500).json({ error: "An unexpected error occurred." }); + } +} + // export const getMentor = async ( // req: Request, diff --git a/functions/src/routes/mentorship.ts b/functions/src/routes/mentorship.ts index 76f3f0e..3f3ca3f 100644 --- a/functions/src/routes/mentorship.ts +++ b/functions/src/routes/mentorship.ts @@ -1,5 +1,5 @@ import express, { Request, Response } from "express"; -import { getMentorshipConfig, hackerBookMentorships, hackerGetMentor, hackerGetMentors, hackerGetMentorSchedule, hackerGetMentorSchedules, mentorGetMyMentorship, mentorGetMyMentorships, mentorPutMyMentorship } from "../controllers/mentorship_controller"; +import { getMentorshipConfig, hackerBookMentorships, hackerCancelMentorship, hackerGetMentor, hackerGetMentors, hackerGetMentorSchedule, hackerGetMentorSchedules, mentorGetMyMentorship, mentorGetMyMentorships, mentorPutMyMentorship } from "../controllers/mentorship_controller"; import { isMentor } from "../middlewares/role_middleware"; const router = express.Router(); @@ -37,6 +37,9 @@ router.get("/hacker/mentorships/:id", async (req: Request, res: Response) => { router.post("/hacker/mentorships/book", async (req: Request, res: Response) => { await hackerBookMentorships(req, res) }) +router.post("/hacker/mentorships/cancel", async (req: Request, res: Response) => { + await hackerCancelMentorship(req, res) +}) // router.get("/mentors", (req: Request, res: Response) => getMentors(req, res)) From 67210c19b0abfa72ba0c52042a9470ba2f98d430 Mon Sep 17 00:00:00 2001 From: Heryan Djaruma Date: Mon, 21 Jul 2025 06:50:52 +0700 Subject: [PATCH 27/40] add get my mentorship hacker --- .../src/controllers/mentorship_controller.ts | 63 ++++++++++++++++--- functions/src/routes/mentorship.ts | 5 +- 2 files changed, 57 insertions(+), 11 deletions(-) diff --git a/functions/src/controllers/mentorship_controller.ts b/functions/src/controllers/mentorship_controller.ts index 178e10f..386b147 100644 --- a/functions/src/controllers/mentorship_controller.ts +++ b/functions/src/controllers/mentorship_controller.ts @@ -404,13 +404,13 @@ export const hackerBookMentorships = async ( return res.status(200).json({ success: true, message: 'Mentorships booked successfully.' }); } catch (error) { const err = error as Error - + if (err.message.includes('mentorship slots could not be found')) { - return res.status(400).json({error: "Mentorship slot(s) could not be found"}) + return res.status(400).json({ error: "Mentorship slot(s) could not be found" }) } else if (err.message.includes('already booked')) { - return res.status(400).json({error: "Mentorship slot(s) are already booked"}) + return res.status(400).json({ error: "Mentorship slot(s) are already booked" }) } else if (err.message.includes('too soon to book')) { - return res.status(400).json({error: "Cannot book less than 30 mins before the mentoring schedule. Please choose another mentorship slot!"}) + return res.status(400).json({ error: "Cannot book less than 30 mins before the mentoring schedule. Please choose another mentorship slot!" }) } functions.logger.error(`Error when trying hackerBookMentorships: ${(error as Error).message}`) @@ -424,12 +424,12 @@ export const hackerCancelMentorship = async ( try { const uid = req.user?.uid if (!uid) { - return res.status(401).json({error: "Unauthorized"}) + return res.status(401).json({ error: "Unauthorized" }) } const { id } = req.body; if (!id) { - return res.status(400).json({error: "id must be in the argument"}) + return res.status(400).json({ error: "id must be in the argument" }) } // Validate @@ -438,11 +438,11 @@ export const hackerCancelMentorship = async ( const mentorshipSnapshot = await db.collection(MENTORSHIPS).doc(id).get() const mentorshipData = mentorshipSnapshot.data() if (!mentorshipSnapshot.exists || !mentorshipData) { - return res.status(404).json({error: "Cannot find mentorship with the given id"}) + return res.status(404).json({ error: "Cannot find mentorship with the given id" }) } - + if (mentorshipData.hackerId !== uid) { - return res.status(401).json({error: "Unauthorized"}) + return res.status(401).json({ error: "Unauthorized" }) } await db.collection(MENTORSHIPS).doc(id).update({ @@ -453,7 +453,7 @@ export const hackerCancelMentorship = async ( offlineLocation: FieldValue.delete(), }) - res.status(200).json({message: "Mentorship has been canceled."}) + res.status(200).json({ message: "Mentorship has been canceled." }) } catch (error) { functions.logger.error(`Error when trying hackerCancelMentorship: ${(error as Error).message}`) return res.status(500).json({ error: "An unexpected error occurred." }); @@ -461,6 +461,49 @@ export const hackerCancelMentorship = async ( } +export const hackerGetMyMentorships = async ( + req: Request, res: Response +) => { + try { + const uid = req.user?.uid + if (!uid) { + return res.status(401).json({ error: "Unauthorized" }) + } + + // available query params + const { + upcomingOnly, + recentOnly + } = req.query; + + let query: CollectionReference | DocumentData = db.collection(MENTORSHIPS); + + query = query.where(HACKER_ID, "==", uid); + + const currentTimeSeconds = Math.floor(DateTime.now().setZone('Asia/Jakarta').toUnixInteger()); + if (upcomingOnly === 'true') { + query = query.where(START_TIME, ">=", currentTimeSeconds); + } else if (recentOnly === 'true') { + query = query.where(START_TIME, "<=", currentTimeSeconds); + } + + const snapshot = await query.orderBy(START_TIME, "asc").get(); + + let mentorships = snapshot.docs.map((doc: any) => ({ + id: doc.id, + ...doc.data(), + })) as MentorshipAppointment[]; + + return res.status(200).json({ + data: mentorships, + }); + } catch (error) { + functions.logger.error(`Error when trying hackerGetMyMentorships: ${(error as Error).message}`) + return res.status(500).json({ error: (error as Error).message }) + } +} + + // export const getMentor = async ( // req: Request, // res: Response diff --git a/functions/src/routes/mentorship.ts b/functions/src/routes/mentorship.ts index 3f3ca3f..6c0c2e8 100644 --- a/functions/src/routes/mentorship.ts +++ b/functions/src/routes/mentorship.ts @@ -1,5 +1,5 @@ import express, { Request, Response } from "express"; -import { getMentorshipConfig, hackerBookMentorships, hackerCancelMentorship, hackerGetMentor, hackerGetMentors, hackerGetMentorSchedule, hackerGetMentorSchedules, mentorGetMyMentorship, mentorGetMyMentorships, mentorPutMyMentorship } from "../controllers/mentorship_controller"; +import { getMentorshipConfig, hackerBookMentorships, hackerCancelMentorship, hackerGetMentor, hackerGetMentors, hackerGetMentorSchedule, hackerGetMentorSchedules, hackerGetMyMentorships, mentorGetMyMentorship, mentorGetMyMentorships, mentorPutMyMentorship } from "../controllers/mentorship_controller"; import { isMentor } from "../middlewares/role_middleware"; const router = express.Router(); @@ -37,6 +37,9 @@ router.get("/hacker/mentorships/:id", async (req: Request, res: Response) => { router.post("/hacker/mentorships/book", async (req: Request, res: Response) => { await hackerBookMentorships(req, res) }) +router.get("/hacker/my-mentorships", async (req: Request, res: Response) => { + await hackerGetMyMentorships(req, res) +}) router.post("/hacker/mentorships/cancel", async (req: Request, res: Response) => { await hackerCancelMentorship(req, res) }) From a606008e29885276846b91b0762499521720e29a Mon Sep 17 00:00:00 2001 From: Heryan Djaruma Date: Mon, 21 Jul 2025 06:53:21 +0700 Subject: [PATCH 28/40] minor fix book mentorship --- .../src/controllers/mentorship_controller.ts | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/functions/src/controllers/mentorship_controller.ts b/functions/src/controllers/mentorship_controller.ts index 386b147..9af3835 100644 --- a/functions/src/controllers/mentorship_controller.ts +++ b/functions/src/controllers/mentorship_controller.ts @@ -320,7 +320,6 @@ export const hackerGetMentorSchedule = async ( interface BookMentorshipRequest { id: string; - hackerId: string; hackerName: string; teamName: string; hackerDescription: string; @@ -333,6 +332,11 @@ export const hackerBookMentorships = async ( ) => { const MAX_CONCURRENT_BOOKINGS = 2 try { + const uid = req.user?.uid + if (!uid) { + return res.status(401).json({error: "Unauthorized"}) + } + const { mentorships }: { mentorships: BookMentorshipRequest[] } = req.body; if (!mentorships || !Array.isArray(mentorships) || mentorships.length === 0) { @@ -343,20 +347,16 @@ export const hackerBookMentorships = async ( return res.status(400).json({ error: `Cannot book more than ${MAX_CONCURRENT_BOOKINGS} slots at a time.` }); } - const hackerId = mentorships[0].hackerId; for (const mentorship of mentorships) { - if (!mentorship.id || !mentorship.hackerId || !mentorship.hackerName || !mentorship.teamName || !mentorship.hackerDescription) { + if (!mentorship.id || !mentorship.hackerName || !mentorship.teamName || !mentorship.hackerDescription) { return res.status(400).json({ error: 'Each mentorship must include id, hackerId, hackerName, teamName, and hackerDescription.' }); } - if (mentorship.hackerId !== hackerId) { - return res.status(400).json({ error: 'All mentorship bookings in a single request must be for the same hacker.' }); - } } const mentorshipsCollection = db.collection(MENTORSHIPS); const currentTimeSeconds = Math.floor(DateTime.now().setZone('Asia/Jakarta').toUnixInteger()); const existingBookingsQuery = mentorshipsCollection - .where(HACKER_ID, '==', hackerId) + .where(HACKER_ID, '==', uid) .where(START_TIME, '>', currentTimeSeconds); const existingBookingsSnapshot = await existingBookingsQuery.get(); @@ -392,7 +392,7 @@ export const hackerBookMentorships = async ( for (const mentorshipRequest of mentorships) { const docRef = mentorshipsCollection.doc(mentorshipRequest.id); transaction.update(docRef, { - hackerId: mentorshipRequest.hackerId, + hackerId: uid, hackerName: mentorshipRequest.hackerName, teamName: mentorshipRequest.teamName, hackerDescription: mentorshipRequest.hackerDescription, From d4f92b0269c7236d6006687282cc4969413be5ee Mon Sep 17 00:00:00 2001 From: Heryan Djaruma Date: Mon, 21 Jul 2025 07:00:06 +0700 Subject: [PATCH 29/40] add hacker get my mentorship endpoint --- .../src/controllers/mentorship_controller.ts | 310 ++---------------- functions/src/routes/mentorship.ts | 9 +- 2 files changed, 31 insertions(+), 288 deletions(-) diff --git a/functions/src/controllers/mentorship_controller.ts b/functions/src/controllers/mentorship_controller.ts index 9af3835..78bbb74 100644 --- a/functions/src/controllers/mentorship_controller.ts +++ b/functions/src/controllers/mentorship_controller.ts @@ -503,289 +503,29 @@ export const hackerGetMyMentorships = async ( } } +export const hackerGetMyMentorship = async ( + req: Request, res: Response +) => { + try { + const uid = req.user?.uid + if (!uid) { + return res.status(401).json({error: "Unauthorized"}) + } -// export const getMentor = async ( -// req: Request, -// res: Response -// ): Promise => { -// const { mentorId } = req.params; -// try { -// /** -// * Validate request: -// * 1. Mentor id is not in params -// */ -// if (!mentorId) { -// res.status(400).json({ -// status: 400, -// error: "Mentor id is required" -// }) -// return; -// } - -// /** -// * Process request -// * 1. Find mentor in db -// */ -// const mentorDoc = await db.collection("users").doc(mentorId).get() -// if (!mentorDoc.exists) { -// res.status(404).json({ -// status: 404, -// error: "Cannot find mentor with the given mentor id" -// }) -// return; -// } -// const mentorData = mentorDoc.data() - -// if (mentorData) { -// const trimmedData: FirestoreMentor = { -// "id": mentorData.id, -// "email": mentorData.email, -// "name": mentorData.name, -// "intro": mentorData.intro, -// "specialization": mentorData.specialization, -// "mentor": mentorData.mentor, -// "discordUsername": mentorData.discordUsername -// } -// res.status(200).json({ -// status: 200, -// data: trimmedData -// }) -// } else { -// res.status(200).json({ -// status: 200, -// data: {} -// }) -// } -// } catch (error) { -// res.status(500).json({ error: (error as Error).message }) -// } -// } - -// /** -// * Get all mentorship appointments. -// * -// * To be used in admin. -// * @param req -// * @param res -// */ -// export const getMentorshipAppointments = async ( -// req: Request, -// res: Response -// ): Promise => { -// const mentorshipAppointments: MentorshipAppointment[] = []; - -// try { -// const snapshot = await db.collection('mentorships') -// .where("mentor", "==", true) -// .get() -// snapshot.docs.map((mentor) => { -// mentorshipAppointments.push({ -// id: mentor.id, -// ...mentor.data() -// } as MentorshipAppointment) -// }) -// res.status(200).json({ mentorshipAppointments }) -// } catch (error) { -// res.status(500).json({ error: (error as Error).message }); -// } -// } - -// /** -// * Get mentorship appointments by mentor id. -// * -// * Use for: -// * 1. Admin -// * 2. Mentor in portal -// * 3. Hacker in portal -// * @param req -// * @param res -// * @returns -// */ -// export const getMentorshipAppointmentsByMentorId = async ( -// req: Request, -// res: Response -// ): Promise => { -// try { -// const { mentorId } = req.params; -// const doc = await db.collection("mentorships") -// .where("mentorId", "==", mentorId) -// .get(); - -// if (doc.empty) { -// res.status(404).json({ error: "Cannot find mentorships related with the mentor." }); -// return; -// } - -// const mentorships: MentorshipAppointment[] = []; -// doc.docs.map((mentorship) => { -// mentorships.push({ -// id: mentorship.id, -// ...mentorship.data() -// } as MentorshipAppointment) -// }) - -// res.status(200).json(mentorships); -// } catch (error) { -// res.status(500).json({ error: (error as Error).message }); -// } -// }; - -// /** -// * Book mentorship appointment. Use request cookie to get hacker uid. -// * -// * Use for: -// * 1. Hacker in portal -// * @param req -// * @param res -// * @returns -// */ -// export const bookAMentorshipAppointment = async ( -// req: Request, -// res: Response -// ): Promise => { -// try { -// /** -// * Validate current user: -// * 1. User is not valid / not authenticated. -// * 2. Get current user from db -// */ -// const uid = req.user?.uid; -// if (!uid || uid === undefined) { -// res.status(401).json({ -// status: 401, -// error: "Cannot get current user uid" -// }) -// return; -// } - -// const { mentorshipAppointmentId, hackerDescription } = req.body - -// // reject if no mentorshipAppointmentId -// if (mentorshipAppointmentId === undefined || !mentorshipAppointmentId) { -// res.status(400).json({ -// status: 400, -// error: "mentorshipAppointmentId is required in body" -// }) -// return -// } - -// /** -// * Validation: -// * 1. Reject if the mentorship appointment does not exist in db -// * 2. Reject if the mentorship is already booked -// */ -// const mentorshipAppointmentDoc = await db.collection("mentorships").doc(mentorshipAppointmentId) -// const mentorshipAppointmentSnap = await mentorshipAppointmentDoc.get() -// if (!mentorshipAppointmentSnap.exists) { -// res.status(400).json({ -// status: 400, -// error: "Mentorship appointment does not exist" -// }) -// return -// } - -// const mentorshipData: MentorshipAppointment | undefined = mentorshipAppointmentSnap.data() as MentorshipAppointment -// if (mentorshipData.hackerId) { -// res.status(400).json({ -// status: 400, -// error: "Mentorship slot is already booked" -// }) -// return -// } - -// /** -// * Book the mentorship slot -// * 1. Update the hackerId for the document -// */ -// const updatedMentorshipAppointment = { -// hackerId: uid, -// hackerDescription: hackerDescription, -// ...mentorshipData -// } -// await mentorshipAppointmentDoc.update(updatedMentorshipAppointment) -// res.status(200).json({ -// status: 200, -// data: "Successfuly booked mentorship slot" -// }) -// } catch (error) { -// res.status(500).json({ error: (error as Error).message }) -// } -// } - - -// /** -// * Get the list of my mentorship appointments. -// * If user is mentor, by default will return his appointment. -// * Use request cookie to get uid. -// * -// * Use for: -// * 1. Mentor in portal -// * 2. Hacker in portal -// * -// * @param req.query.upcomingOnly boolean : If or not only fetches upcoming only. -// * @param req.query.recentOnly boolean : If or not only fetches recent only. -// */ - -// export const getMyMentorshipAppointments = async ( -// req: Request, -// res: Response -// ): Promise => { -// try { -// // 1. Validate User -// const uid = req.user?.uid; -// if (!uid) { -// res.status(401).json({ status: 401, error: "Cannot get current user uid" }); -// return; -// } - -// const currentUserSnap = await db.collection("users").doc(uid).get(); -// if (!currentUserSnap.exists) { -// res.status(400).json({ status: 400, error: "User not found" }); -// return; -// } - -// const currentUserData = currentUserSnap.data() as FirestoreMentor | User; -// const upcomingOnly = req.query.upcomingOnly === 'true'; -// const recentOnly = req.query.recentOnly === 'true'; - -// // 2. Build Query Dynamically -// let query = db.collection("mentorships"); - -// if (isMentor(currentUserData)) { -// query = query.where("mentorId", "==", uid) as CollectionReference; -// } else { -// query = query.where("hackerId", "==", uid) as CollectionReference;; -// } - -// const currentTimeSeconds = Math.floor(DateTime.now().setZone('Asia/Jakarta').toUnixInteger()); -// if (upcomingOnly) { -// query = query.where("startTime", ">=", currentTimeSeconds) as CollectionReference; -// } else if (recentOnly) { -// query = query.where("startTime", "<=", currentTimeSeconds) as CollectionReference; -// } - -// // 3. Execute Query and Send Response -// const snapshot = await query.orderBy("startTime", "asc").get(); - -// const mentorshipAppointments = snapshot.docs.map((doc) => ({ -// id: doc.id, -// ...doc.data(), -// })) as MentorshipAppointment[]; - -// res.status(200).json({ -// status: 200, -// data: mentorshipAppointments, -// }); -// } catch (error) { -// res.status(500).json({ error: (error as Error).message }); -// } -// }; - -// /** -// * -// * @param data -// * @returns -// */ -// function isMentor(data: FirestoreMentor | User): data is FirestoreMentor { -// return 'mentor' in data; -// } \ No newline at end of file + const { id } = req.params + if (!id) { + return res.status(400).json({error: "id is required as argument"}) + } + + const snapshot = await db.collection(MENTORSHIPS).doc(id).get() + const data = snapshot.data() + if (!snapshot.exists || !data) { + return res.status(404).json({error: "Cannot find mentorship"}) + } + + return res.status(200).json({data: data}) + } catch (error) { + functions.logger.error(`Error when trying hackerGetMyMentorship: ${(error as Error).message}`) + return res.status(500).json({ error: (error as Error).message }) + } +} \ No newline at end of file diff --git a/functions/src/routes/mentorship.ts b/functions/src/routes/mentorship.ts index 6c0c2e8..370eb10 100644 --- a/functions/src/routes/mentorship.ts +++ b/functions/src/routes/mentorship.ts @@ -1,5 +1,5 @@ import express, { Request, Response } from "express"; -import { getMentorshipConfig, hackerBookMentorships, hackerCancelMentorship, hackerGetMentor, hackerGetMentors, hackerGetMentorSchedule, hackerGetMentorSchedules, hackerGetMyMentorships, mentorGetMyMentorship, mentorGetMyMentorships, mentorPutMyMentorship } from "../controllers/mentorship_controller"; +import { getMentorshipConfig, hackerBookMentorships, hackerCancelMentorship, hackerGetMentor, hackerGetMentors, hackerGetMentorSchedule, hackerGetMentorSchedules, hackerGetMyMentorship, hackerGetMyMentorships, mentorGetMyMentorship, mentorGetMyMentorships, mentorPutMyMentorship } from "../controllers/mentorship_controller"; import { isMentor } from "../middlewares/role_middleware"; const router = express.Router(); @@ -37,11 +37,14 @@ router.get("/hacker/mentorships/:id", async (req: Request, res: Response) => { router.post("/hacker/mentorships/book", async (req: Request, res: Response) => { await hackerBookMentorships(req, res) }) +router.post("/hacker/mentorships/cancel", async (req: Request, res: Response) => { + await hackerCancelMentorship(req, res) +}) router.get("/hacker/my-mentorships", async (req: Request, res: Response) => { await hackerGetMyMentorships(req, res) }) -router.post("/hacker/mentorships/cancel", async (req: Request, res: Response) => { - await hackerCancelMentorship(req, res) +router.get("/hacker/my-mentorships/:id", async (req: Request, res: Response) => { + await hackerGetMyMentorship(req, res) }) From 18f03015fecf1b35726d2b0035b39989121383b2 Mon Sep 17 00:00:00 2001 From: Heryan Djaruma Date: Mon, 21 Jul 2025 07:00:51 +0700 Subject: [PATCH 30/40] minor fix --- functions/src/controllers/mentorship_controller.ts | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/functions/src/controllers/mentorship_controller.ts b/functions/src/controllers/mentorship_controller.ts index 78bbb74..52d6f0b 100644 --- a/functions/src/controllers/mentorship_controller.ts +++ b/functions/src/controllers/mentorship_controller.ts @@ -1,7 +1,6 @@ import { db } from "../config/firebase" import { FirestoreMentor, MentorshipAppointment, MentorshipAppointmentResponseAsHacker } from "../models/mentorship"; import { Request, Response } from "express"; -import { User } from "../models/user"; import { DateTime } from 'luxon'; import { CollectionReference, DocumentData, FieldPath, FieldValue } from "firebase-admin/firestore"; import { MentorshipConfig } from "../types/config"; @@ -489,7 +488,7 @@ export const hackerGetMyMentorships = async ( const snapshot = await query.orderBy(START_TIME, "asc").get(); - let mentorships = snapshot.docs.map((doc: any) => ({ + const mentorships = snapshot.docs.map((doc: any) => ({ id: doc.id, ...doc.data(), })) as MentorshipAppointment[]; From f485563093def5537aa4e7bb20f6e020a636ff00 Mon Sep 17 00:00:00 2001 From: Heryan Djaruma Date: Mon, 21 Jul 2025 07:10:09 +0700 Subject: [PATCH 31/40] remove unused code --- functions/src/routes/mentorship.ts | 7 ------- 1 file changed, 7 deletions(-) diff --git a/functions/src/routes/mentorship.ts b/functions/src/routes/mentorship.ts index 370eb10..8191a0c 100644 --- a/functions/src/routes/mentorship.ts +++ b/functions/src/routes/mentorship.ts @@ -47,11 +47,4 @@ router.get("/hacker/my-mentorships/:id", async (req: Request, res: Response) => await hackerGetMyMentorship(req, res) }) - -// router.get("/mentors", (req: Request, res: Response) => getMentors(req, res)) -// router.get("/mentors/:mentorId", (req: Request, res: Response) => getMentor(req, res)) -// router.get("/mentorships/:mentorId", (req: Request, res: Response) => getMentorshipAppointmentsByMentorId(req, res)) -// router.post("/mentorships", (req: Request, res: Response) => bookAMentorshipAppointment(req, res)) -// router.get("/my-mentorships", (req: Request, res: Response) => getMyMentorshipAppointments(req, res)) - export default router; \ No newline at end of file From 5c35b3bf4bbc82bc4dcde025fc1617a69e636041 Mon Sep 17 00:00:00 2001 From: Heryan Djaruma Date: Mon, 21 Jul 2025 07:20:26 +0700 Subject: [PATCH 32/40] add validation before book mentorship --- .../src/controllers/mentorship_controller.ts | 17 +++++++++++++++-- 1 file changed, 15 insertions(+), 2 deletions(-) diff --git a/functions/src/controllers/mentorship_controller.ts b/functions/src/controllers/mentorship_controller.ts index 52d6f0b..763b3ae 100644 --- a/functions/src/controllers/mentorship_controller.ts +++ b/functions/src/controllers/mentorship_controller.ts @@ -6,6 +6,8 @@ import { CollectionReference, DocumentData, FieldPath, FieldValue } from "fireba import { MentorshipConfig } from "../types/config"; import * as functions from "firebase-functions"; +const CONFIG = "config"; +const MENTORSHIP_CONIFG = "mentorshipConfig"; const MENTORSHIPS = "mentorships"; const MENTOR_ID = "mentorId"; const HACKER_ID = "hackerId"; @@ -336,6 +338,13 @@ export const hackerBookMentorships = async ( return res.status(401).json({error: "Unauthorized"}) } + // Check if mentorship is open + const configSnapshot = await db.collection(CONFIG).doc(MENTORSHIP_CONIFG).get() + const configData = configSnapshot.data() + if (configData && !configData.isMentorshipOpen) { + return res.status(400).json({error: "Mentorship is currently closed"}) + } + const { mentorships }: { mentorships: BookMentorshipRequest[] } = req.body; if (!mentorships || !Array.isArray(mentorships) || mentorships.length === 0) { @@ -510,17 +519,21 @@ export const hackerGetMyMentorship = async ( if (!uid) { return res.status(401).json({error: "Unauthorized"}) } - + const { id } = req.params if (!id) { return res.status(400).json({error: "id is required as argument"}) } - + const snapshot = await db.collection(MENTORSHIPS).doc(id).get() const data = snapshot.data() if (!snapshot.exists || !data) { return res.status(404).json({error: "Cannot find mentorship"}) } + + if (data.mentorId !== uid) { + return res.status(401).json({error: "Unauthorized"}) + } return res.status(200).json({data: data}) } catch (error) { From 4913985d06b78bfef1a3c94a5b8a57c4b9f81279 Mon Sep 17 00:00:00 2001 From: Heryan Djaruma Date: Mon, 21 Jul 2025 11:58:12 +0700 Subject: [PATCH 33/40] minor fix return data mentor --- functions/src/controllers/mentorship_controller.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/functions/src/controllers/mentorship_controller.ts b/functions/src/controllers/mentorship_controller.ts index 763b3ae..bbed765 100644 --- a/functions/src/controllers/mentorship_controller.ts +++ b/functions/src/controllers/mentorship_controller.ts @@ -275,6 +275,7 @@ export const hackerGetMentorSchedules = async ( endTime: doc.data().endTime, mentorId: doc.data().mentorId, hackerId: doc.data().hackerId, + location: doc.data().location, })) as MentorshipAppointmentResponseAsHacker[]; return res.status(200).json({ data: allSchedules }); From 8e897a72a9a0e66565eb4c174769ccf4e6140d74 Mon Sep 17 00:00:00 2001 From: Heryan Djaruma Date: Mon, 21 Jul 2025 16:55:24 +0700 Subject: [PATCH 34/40] minor fix add check 25 mins --- .../src/controllers/mentorship_controller.ts | 28 +++++++++++-------- 1 file changed, 17 insertions(+), 11 deletions(-) diff --git a/functions/src/controllers/mentorship_controller.ts b/functions/src/controllers/mentorship_controller.ts index bbed765..4bcaaa8 100644 --- a/functions/src/controllers/mentorship_controller.ts +++ b/functions/src/controllers/mentorship_controller.ts @@ -336,14 +336,14 @@ export const hackerBookMentorships = async ( try { const uid = req.user?.uid if (!uid) { - return res.status(401).json({error: "Unauthorized"}) + return res.status(401).json({ error: "Unauthorized" }) } // Check if mentorship is open const configSnapshot = await db.collection(CONFIG).doc(MENTORSHIP_CONIFG).get() const configData = configSnapshot.data() if (configData && !configData.isMentorshipOpen) { - return res.status(400).json({error: "Mentorship is currently closed"}) + return res.status(400).json({ error: "Mentorship is currently closed" }) } const { mentorships }: { mentorships: BookMentorshipRequest[] } = req.body; @@ -454,6 +454,12 @@ export const hackerCancelMentorship = async ( return res.status(401).json({ error: "Unauthorized" }) } + // handle if booking is aleady 45 mins away + const fortyFiveMinsFromNow = Math.floor(DateTime.now().setZone('Asia/Jakarta').toUnixInteger()) + (45 * 60); + if (mentorshipData.startTime < fortyFiveMinsFromNow) { + return res.status(400).json({ error: "Mentorship cannot be canceled less than 45 minutes before schedule."}) + } + await db.collection(MENTORSHIPS).doc(id).update({ hackerId: FieldValue.delete(), hackerName: FieldValue.delete(), @@ -462,7 +468,7 @@ export const hackerCancelMentorship = async ( offlineLocation: FieldValue.delete(), }) - res.status(200).json({ message: "Mentorship has been canceled." }) + return res.status(200).json({ message: "Mentorship has been canceled." }) } catch (error) { functions.logger.error(`Error when trying hackerCancelMentorship: ${(error as Error).message}`) return res.status(500).json({ error: "An unexpected error occurred." }); @@ -518,25 +524,25 @@ export const hackerGetMyMentorship = async ( try { const uid = req.user?.uid if (!uid) { - return res.status(401).json({error: "Unauthorized"}) + return res.status(401).json({ error: "Unauthorized" }) } - + const { id } = req.params if (!id) { - return res.status(400).json({error: "id is required as argument"}) + return res.status(400).json({ error: "id is required as argument" }) } - + const snapshot = await db.collection(MENTORSHIPS).doc(id).get() const data = snapshot.data() if (!snapshot.exists || !data) { - return res.status(404).json({error: "Cannot find mentorship"}) + return res.status(404).json({ error: "Cannot find mentorship" }) } - + if (data.mentorId !== uid) { - return res.status(401).json({error: "Unauthorized"}) + return res.status(401).json({ error: "Unauthorized" }) } - return res.status(200).json({data: data}) + return res.status(200).json({ data: data }) } catch (error) { functions.logger.error(`Error when trying hackerGetMyMentorship: ${(error as Error).message}`) return res.status(500).json({ error: (error as Error).message }) From 10575b9053be3f3b93652d229a3c66c2287f8f68 Mon Sep 17 00:00:00 2001 From: Heryan Djaruma Date: Mon, 21 Jul 2025 19:29:15 +0700 Subject: [PATCH 35/40] minor fix add limit --- functions/src/controllers/mentorship_controller.ts | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/functions/src/controllers/mentorship_controller.ts b/functions/src/controllers/mentorship_controller.ts index 4bcaaa8..cb7c14d 100644 --- a/functions/src/controllers/mentorship_controller.ts +++ b/functions/src/controllers/mentorship_controller.ts @@ -55,6 +55,7 @@ export const mentorGetMyMentorships = async ( // available query params const { + limit, upcomingOnly, recentOnly, isBooked, @@ -72,6 +73,13 @@ export const mentorGetMyMentorships = async ( query = query.where(START_TIME, "<=", currentTimeSeconds); } + if (limit) { + const numericLimit = parseInt(limit as string, 10); + if (!isNaN(numericLimit) && numericLimit > 0) { + query = query.limit(numericLimit); + } + } + const snapshot = await query.orderBy("startTime", "asc").get(); let mentorships = snapshot.docs.map((doc: any) => ({ @@ -457,7 +465,7 @@ export const hackerCancelMentorship = async ( // handle if booking is aleady 45 mins away const fortyFiveMinsFromNow = Math.floor(DateTime.now().setZone('Asia/Jakarta').toUnixInteger()) + (45 * 60); if (mentorshipData.startTime < fortyFiveMinsFromNow) { - return res.status(400).json({ error: "Mentorship cannot be canceled less than 45 minutes before schedule."}) + return res.status(400).json({ error: "Mentorship cannot be canceled less than 45 minutes before schedule." }) } await db.collection(MENTORSHIPS).doc(id).update({ From 5a21a2159ba83003542c9882005d79c68d0a57b1 Mon Sep 17 00:00:00 2001 From: Heryan Djaruma Date: Tue, 22 Jul 2025 13:21:23 +0700 Subject: [PATCH 36/40] add check for mentorship is open --- functions/src/controllers/mentorship_controller.ts | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/functions/src/controllers/mentorship_controller.ts b/functions/src/controllers/mentorship_controller.ts index cb7c14d..62dd958 100644 --- a/functions/src/controllers/mentorship_controller.ts +++ b/functions/src/controllers/mentorship_controller.ts @@ -261,6 +261,13 @@ export const hackerGetMentorSchedules = async ( try { const { mentorId, limit } = req.query + // Check if mentorship is open + const configSnapshot = await db.collection(CONFIG).doc(MENTORSHIP_CONIFG).get() + const configData = configSnapshot.data() + if (configData && !configData.isMentorshipOpen) { + return res.status(400).json({ error: "Mentorship is currently closed" }) + } + if (!mentorId) { return res.status(400).json({ error: "mentorId is required as argument" }) } From 8f3046a724da281662615a82312e333488480c13 Mon Sep 17 00:00:00 2001 From: Heryan Djaruma Date: Tue, 22 Jul 2025 15:38:03 +0700 Subject: [PATCH 37/40] success send email mentor for booking --- .../src/controllers/mentorship_controller.ts | 158 ++++++++++++++++-- functions/src/server.ts | 2 +- functions/src/utils/date.ts | 21 +++ 3 files changed, 170 insertions(+), 11 deletions(-) create mode 100644 functions/src/utils/date.ts diff --git a/functions/src/controllers/mentorship_controller.ts b/functions/src/controllers/mentorship_controller.ts index 62dd958..f41786f 100644 --- a/functions/src/controllers/mentorship_controller.ts +++ b/functions/src/controllers/mentorship_controller.ts @@ -5,6 +5,9 @@ import { DateTime } from 'luxon'; import { CollectionReference, DocumentData, FieldPath, FieldValue } from "firebase-admin/firestore"; import { MentorshipConfig } from "../types/config"; import * as functions from "firebase-functions"; +import nodemailer from "nodemailer"; +import { epochToStringDate } from "../utils/date"; + const CONFIG = "config"; const MENTORSHIP_CONIFG = "mentorshipConfig"; @@ -14,6 +17,102 @@ const HACKER_ID = "hackerId"; const USERS = "users"; const START_TIME = "startTime"; +const transporter = nodemailer.createTransport({ + host: "live.smtp.mailtrap.io", + port: 587, + auth: { + user: process.env.MAILTRAP_USER, + pass: process.env.MAILTRAP_PASS, + }, +}); + +interface MailOptions { + from: string | { name: string; address: string }; + to: string; + subject: string; + html: string; + text: string; +} + +const createMentorshipBookingMailOptions = ( + mentorEmail: string, + mentorName: string, + teamName: string, + hackerName: string, + startDate: string, + endDate: string, + portalLink: string, + duration: number, +): MailOptions => ({ + from: { + name: "Garuda Hacks", + address: "no-reply@garudahacks.com" + }, + to: mentorEmail, + subject: `Team ${teamName} Just Booked A Mentorship Session`, + html: ` + + + + + + Mentorship Booking + + + + +
+

Hi, ${mentorName}

+

A team just booked a mentorship session with you.

+ +
+
+

Team Name ${teamName}

+

Hacker Name ${hackerName}

+
+ +
+

${startDate} - ${endDate} (${duration} minutes)

+
+ +
+

Click here to view portal.

+ View + In Portal +
+
+
+
+

© ${new Date().getFullYear()} Garuda Hacks. All rights reserved.

+

+ Visit our website | + Contact Support +

+
+ + + `, + text: `Team ${teamName} Just Booked A Mentorship Session.` +}) + +const sendMentorshipBookingEmail = async ( + mentorEmail: string, + mentorName: string, + teamName: string, + hackerName: string, + startDate: string, + endDate: string, + portalLink: string, + duration: number, +): Promise => { + const mailOptions = createMentorshipBookingMailOptions(mentorEmail, mentorName, teamName, hackerName, startDate, endDate, portalLink, duration) + await transporter.sendMail(mailOptions) + functions.logger.info("Booking email sent successfuly to:", mentorEmail) +} + /** * Get mentorship config. */ @@ -97,7 +196,7 @@ export const mentorGetMyMentorships = async ( data: mentorships, }); } catch (error) { - functions.logger.error(`Error when trying mentorGetMyMentorships: ${(error as Error).message}`) + functions.logger.error(`Error when trying mentorGetMyMentorships: ${(error as Error).message} `) return res.status(500).json({ error: (error as Error).message }) } } @@ -128,7 +227,7 @@ export const mentorGetMyMentorship = async ( data: snapshot.data() }) } catch (error) { - functions.logger.error(`Error when trying mentorGetMyMentorship: ${(error as Error).message}`) + functions.logger.error(`Error when trying mentorGetMyMentorship: ${(error as Error).message} `) return res.status(500).json({ error: (error as Error).message }) } } @@ -225,7 +324,7 @@ export const hackerGetMentors = async ( }) return res.status(200).json({ data: allMentors }) } catch (error: any) { - functions.logger.error(`Error when trying hackerGetMentors: ${(error as Error).message}`) + functions.logger.error(`Error when trying hackerGetMentors: ${(error as Error).message} `) return res.status(500).json({ error: "An unexpected error occurred." }); } } @@ -249,7 +348,7 @@ export const hackerGetMentor = async ( return res.status(200).json({ data: data }) } catch (error) { - functions.logger.error(`Error when trying hackerGetMentor: ${(error as Error).message}`) + functions.logger.error(`Error when trying hackerGetMentor: ${(error as Error).message} `) return res.status(500).json({ error: "An unexpected error occurred." }); } } @@ -295,7 +394,7 @@ export const hackerGetMentorSchedules = async ( return res.status(200).json({ data: allSchedules }); } catch (error) { - functions.logger.error(`Error when trying hackerGetMentorSchedules: ${(error as Error).message}`) + functions.logger.error(`Error when trying hackerGetMentorSchedules: ${(error as Error).message} `) return res.status(500).json({ error: "An unexpected error occurred." }); } } @@ -330,7 +429,7 @@ export const hackerGetMentorSchedule = async ( } return res.status(200).json({ data: mentorshipAppointmentResponseAsHacker }) } catch (error) { - functions.logger.error(`Error when trying hackerGetMentorSchedules: ${(error as Error).message}`) + functions.logger.error(`Error when trying hackerGetMentorSchedules: ${(error as Error).message} `) return res.status(500).json({ error: "An unexpected error occurred." }); } } @@ -425,6 +524,45 @@ export const hackerBookMentorships = async ( } }); + for (const mentorship of mentorships) { + try { + const mentorshipSnap = await db.collection(MENTORSHIPS).doc(mentorship.id).get() + if (!mentorshipSnap.exists) { + functions.logger.error("Mentorship document not found:", mentorship.id) + continue + } + + const mentorshipData = mentorshipSnap.data() as MentorshipAppointment + + if (!mentorshipData.mentorId) { + functions.logger.error("No mentor ID in mentorship data") + continue + } + + const mentorSnap = await db.collection(USERS).doc(mentorshipData.mentorId).get() + if (!mentorSnap.exists) { + functions.logger.error("Mentor not found:", mentorshipData.id) + continue + } + + const mentorData = mentorSnap.data() as FirestoreMentor + + await sendMentorshipBookingEmail( + mentorData.email, + mentorData.name, + mentorship.teamName, + mentorship.hackerName, + `${epochToStringDate(mentorshipData.startTime)}`, + `${epochToStringDate(mentorshipData.endTime)}`, + "https://portal.garudahacks.com", + (mentorshipData.endTime - mentorshipData.startTime) / 60 + ) + functions.logger.info(`Email sent successfully for mentor ${mentorData.email}:`) + } catch (error) { + functions.logger.error(`Error when trying to send email for mentorship ${mentorship.id}: ${(error as Error).message}`) + } + } + return res.status(200).json({ success: true, message: 'Mentorships booked successfully.' }); } catch (error) { const err = error as Error @@ -437,7 +575,7 @@ export const hackerBookMentorships = async ( return res.status(400).json({ error: "Cannot book less than 30 mins before the mentoring schedule. Please choose another mentorship slot!" }) } - functions.logger.error(`Error when trying hackerBookMentorships: ${(error as Error).message}`) + functions.logger.error(`Error when trying hackerBookMentorships: ${(error as Error).message} `) return res.status(500).json({ error: "An unexpected error occurred." }); } } @@ -485,7 +623,7 @@ export const hackerCancelMentorship = async ( return res.status(200).json({ message: "Mentorship has been canceled." }) } catch (error) { - functions.logger.error(`Error when trying hackerCancelMentorship: ${(error as Error).message}`) + functions.logger.error(`Error when trying hackerCancelMentorship: ${(error as Error).message} `) return res.status(500).json({ error: "An unexpected error occurred." }); } } @@ -528,7 +666,7 @@ export const hackerGetMyMentorships = async ( data: mentorships, }); } catch (error) { - functions.logger.error(`Error when trying hackerGetMyMentorships: ${(error as Error).message}`) + functions.logger.error(`Error when trying hackerGetMyMentorships: ${(error as Error).message} `) return res.status(500).json({ error: (error as Error).message }) } } @@ -559,7 +697,7 @@ export const hackerGetMyMentorship = async ( return res.status(200).json({ data: data }) } catch (error) { - functions.logger.error(`Error when trying hackerGetMyMentorship: ${(error as Error).message}`) + functions.logger.error(`Error when trying hackerGetMyMentorship: ${(error as Error).message} `) return res.status(500).json({ error: (error as Error).message }) } } \ No newline at end of file diff --git a/functions/src/server.ts b/functions/src/server.ts index 2ebe26e..99dfb38 100644 --- a/functions/src/server.ts +++ b/functions/src/server.ts @@ -104,7 +104,7 @@ app.use((req: Request, res: Response, next: NextFunction) => { } const timestamp = new Date().toISOString(); - functions.logger.info( + functions.logger.debug( `[${timestamp}] Incoming Request Details: ${JSON.stringify( logData, null, diff --git a/functions/src/utils/date.ts b/functions/src/utils/date.ts new file mode 100644 index 0000000..a71c17c --- /dev/null +++ b/functions/src/utils/date.ts @@ -0,0 +1,21 @@ +/** + * Convert date to time + * @param date + * @returns + */ +export function dateToStringTime(date: Date) { + return date.toLocaleString('en-US', { timeStyle: 'short' }) +} + +/** + * Convert date to readable string + * @param epochSecond + * @returns + */ +export function epochToStringDate(epochSecond: number) { + const startDate = new Date(epochSecond * 1000) + const startDay = startDate.toLocaleDateString() + const startTimestamp = startDate.toLocaleString('en-US', { timeStyle: 'short' }) + const start = `${startDay} ${startTimestamp}` + return start +} \ No newline at end of file From 5656e7fb73809699eb4175b56de3ba474cd21672 Mon Sep 17 00:00:00 2001 From: Heryan Djaruma Date: Tue, 22 Jul 2025 16:04:37 +0700 Subject: [PATCH 38/40] add cancel book mentor --- .../src/controllers/mentorship_controller.ts | 169 ++++++++++++++---- 1 file changed, 136 insertions(+), 33 deletions(-) diff --git a/functions/src/controllers/mentorship_controller.ts b/functions/src/controllers/mentorship_controller.ts index f41786f..19b9f9d 100644 --- a/functions/src/controllers/mentorship_controller.ts +++ b/functions/src/controllers/mentorship_controller.ts @@ -1,5 +1,5 @@ import { db } from "../config/firebase" -import { FirestoreMentor, MentorshipAppointment, MentorshipAppointmentResponseAsHacker } from "../models/mentorship"; +import { FirestoreMentor, MentorshipAppointment, MentorshipAppointmentResponseAsHacker, MentorshipAppointmentResponseAsMentor } from "../models/mentorship"; import { Request, Response } from "express"; import { DateTime } from 'luxon'; import { CollectionReference, DocumentData, FieldPath, FieldValue } from "firebase-admin/firestore"; @@ -34,6 +34,73 @@ interface MailOptions { text: string; } +const createMentorshipCancelMailOptions = ( + mentorEmail: string, + mentorName: string, + teamName: string, + hackerName: string, + startDate: string, + endDate: string, + portalLink: string, + duration: number +): MailOptions => ({ + from: { + name: "Garuda Hacks", + address: "no-reply@garudahacks.com" + }, + to: mentorEmail, + subject: `Team ${teamName} Just Canceled A Mentorship Session`, + html: ` + + + + + + + Mentorship Booking Canceled + + + + +
+

Hi, ${mentorName}

+

The booking for team ${teamName} has been canceled. +

+
+
+

Team Name: ${teamName}

+

Hacker Name: ${hackerName}

+
+ +
+

${startDate} - ${endDate} (${duration} minutes)

+
+ + +
+
+ The cancelation is permissible up to 45 minutes before the scheduled time. +
+
+
+

© ${new Date().getFullYear()} Garuda Hacks. All rights reserved.

+

+ Visit our website | + Contact Support +

+
+ + +`, + text: `Team ${teamName} Just Canceled A Mentorship Session.` +}) + const createMentorshipBookingMailOptions = ( mentorEmail: string, mentorName: string, @@ -60,40 +127,41 @@ const createMentorshipBookingMailOptions = ( - -
-

Hi, ${mentorName}

-

A team just booked a mentorship session with you.

- -
-
-

Team Name ${teamName}

-

Hacker Name ${hackerName}

+ +
+

Hi, ${mentorName}

+

A team just booked a mentorship session with you.

+ +
+
+

Team Name ${teamName}

+

Hacker Name ${hackerName}

+
+ +
+

${startDate} - ${endDate} (${duration})

+
+ +
+

Click here to view portal.

+ View + In Portal +
-
-

${startDate} - ${endDate} (${duration} minutes)

-
- -
-

Click here to view portal.

- View - In Portal -
-
-
-

© ${new Date().getFullYear()} Garuda Hacks. All rights reserved.

-

- Visit our website | - Contact Support -

-
- - +
+

© ${new Date().getFullYear()} Garuda Hacks. All rights reserved.

+

+ Visit our website | + Contact Support +

+
+ + `, text: `Team ${teamName} Just Booked A Mentorship Session.` }) @@ -111,6 +179,22 @@ const sendMentorshipBookingEmail = async ( const mailOptions = createMentorshipBookingMailOptions(mentorEmail, mentorName, teamName, hackerName, startDate, endDate, portalLink, duration) await transporter.sendMail(mailOptions) functions.logger.info("Booking email sent successfuly to:", mentorEmail) + functions.logger.info("Booking email sent successfuly to:", mentorEmail) +} + +const sendMentorshipCancelEmail = async ( + mentorEmail: string, + mentorName: string, + teamName: string, + hackerName: string, + startDate: string, + endDate: string, + portalLink: string, + duration: number, +): Promise => { + const mailOptions = createMentorshipCancelMailOptions(mentorEmail, mentorName, teamName, hackerName, startDate, endDate, portalLink, duration) + await transporter.sendMail(mailOptions) + functions.logger.info("Cancel email sent successfuly to:", mentorEmail) } /** @@ -598,7 +682,7 @@ export const hackerCancelMentorship = async ( // 1. If mentorship does not exist // 2. If mentorship does not belong to the hacker const mentorshipSnapshot = await db.collection(MENTORSHIPS).doc(id).get() - const mentorshipData = mentorshipSnapshot.data() + const mentorshipData = mentorshipSnapshot.data() as MentorshipAppointmentResponseAsMentor if (!mentorshipSnapshot.exists || !mentorshipData) { return res.status(404).json({ error: "Cannot find mentorship with the given id" }) } @@ -613,6 +697,25 @@ export const hackerCancelMentorship = async ( return res.status(400).json({ error: "Mentorship cannot be canceled less than 45 minutes before schedule." }) } + + // get mentor data + const mentorSnapshot = await db.collection(USERS).doc(mentorshipData.mentorId).get() + const mentorData = mentorSnapshot.data() + + if (mentorData && mentorshipData.teamName && mentorshipData.hackerName) { + // sendEmail + await sendMentorshipCancelEmail( + mentorData.email, + mentorData.name, + mentorshipData.teamName, + mentorshipData.hackerName, + `${epochToStringDate(mentorshipData.startTime)}`, + `${epochToStringDate(mentorshipData.endTime)}`, + "https://portal.garudahacks.com", + (mentorshipData.endTime - mentorshipData.startTime) / 60 + ) + } + await db.collection(MENTORSHIPS).doc(id).update({ hackerId: FieldValue.delete(), hackerName: FieldValue.delete(), From a7810413e637e4e4395586e9beabea48e52644f4 Mon Sep 17 00:00:00 2001 From: Heryan Djaruma Date: Wed, 23 Jul 2025 17:21:58 +0700 Subject: [PATCH 39/40] add available field to hacker fetch mentors --- .../src/controllers/mentorship_controller.ts | 34 +++++++++++++++---- functions/src/models/mentorship.ts | 2 ++ 2 files changed, 29 insertions(+), 7 deletions(-) diff --git a/functions/src/controllers/mentorship_controller.ts b/functions/src/controllers/mentorship_controller.ts index 19b9f9d..9621de4 100644 --- a/functions/src/controllers/mentorship_controller.ts +++ b/functions/src/controllers/mentorship_controller.ts @@ -387,7 +387,6 @@ export const hackerGetMentors = async ( try { const { limit } = req.query - const allMentors: FirestoreMentor[] = []; let query = db.collection('users') .where("mentor", "==", true) @@ -399,13 +398,34 @@ export const hackerGetMentors = async ( } const snapshot = await query.get() + const allMentors: FirestoreMentor[] = []; - snapshot.docs.map((mentor) => { - allMentors.push({ - id: mentor.id, - ...mentor.data() - } as FirestoreMentor) - }) + await Promise.all( + snapshot.docs.map(async (mentor) => { + try { + const mentorshipQuery = db + .collection(MENTORSHIPS) + .where(MENTOR_ID, '==', mentor.id) + .where(HACKER_ID, '==', null); + + const countDoc = await mentorshipQuery.count().get(); + const mentorshipCount = countDoc.data().count; + + allMentors.push({ + id: mentor.id, + available: mentorshipCount, + ...mentor.data(), + } as FirestoreMentor); + } catch (error: any) { + functions.logger.error(`Error fetching availability for mentor ${mentor.id}: ${error.message}`); + allMentors.push({ + id: mentor.id, + available: 0, + ...mentor.data(), + } as FirestoreMentor); + } + }) + ); return res.status(200).json({ data: allMentors }) } catch (error: any) { functions.logger.error(`Error when trying hackerGetMentors: ${(error as Error).message} `) diff --git a/functions/src/models/mentorship.ts b/functions/src/models/mentorship.ts index 7bb5358..5080fa9 100644 --- a/functions/src/models/mentorship.ts +++ b/functions/src/models/mentorship.ts @@ -6,6 +6,8 @@ export interface FirestoreMentor { specialization: string; discordUsername: string; intro: string; // introduction given by mentor + + available?: number // to represent how many slots available } export interface MentorshipAppointment { From 172fcf20e15a1455fcec0ddc877dee6cadf74cf0 Mon Sep 17 00:00:00 2001 From: Heryan Djaruma Date: Thu, 24 Jul 2025 00:25:36 +0700 Subject: [PATCH 40/40] edit mask data --- .../src/controllers/mentorship_controller.ts | 63 ++++++++++++------- 1 file changed, 39 insertions(+), 24 deletions(-) diff --git a/functions/src/controllers/mentorship_controller.ts b/functions/src/controllers/mentorship_controller.ts index 9621de4..21634e5 100644 --- a/functions/src/controllers/mentorship_controller.ts +++ b/functions/src/controllers/mentorship_controller.ts @@ -398,32 +398,31 @@ export const hackerGetMentors = async ( } const snapshot = await query.get() - const allMentors: FirestoreMentor[] = []; + const allMentors: { + id?: string; + email: string; + name: string; + mentor: boolean; + specialization: string; + discordUsername: string; + intro: string; // introduction given by mentor + + }[] = []; await Promise.all( snapshot.docs.map(async (mentor) => { - try { - const mentorshipQuery = db - .collection(MENTORSHIPS) - .where(MENTOR_ID, '==', mentor.id) - .where(HACKER_ID, '==', null); - - const countDoc = await mentorshipQuery.count().get(); - const mentorshipCount = countDoc.data().count; - - allMentors.push({ - id: mentor.id, - available: mentorshipCount, - ...mentor.data(), - } as FirestoreMentor); - } catch (error: any) { - functions.logger.error(`Error fetching availability for mentor ${mentor.id}: ${error.message}`); - allMentors.push({ - id: mentor.id, - available: 0, - ...mentor.data(), - } as FirestoreMentor); - } + const mentorData = mentor.data(); + + allMentors.push({ + id: mentor.id, + email: mentorData.email, + name: mentorData.name, + mentor: mentorData.mentor, + specialization: mentorData.specialization, + discordUsername: mentorData.discordUsername, + intro: mentorData.intro, + }); + }) ); return res.status(200).json({ data: allMentors }) @@ -450,7 +449,23 @@ export const hackerGetMentor = async ( const data = snapshot.data() - return res.status(200).json({ data: data }) + if (!data) { + return res.status(400).json({ + error: "Cannot find mentor" + }) + } + + return res.status(200).json({ + data: { + id: data.id, + email: data.email, + name: data.name, + mentor: data.mentor, + specialization: data.specialization, + discordUsername: data.discordUsername, + intro: data.intro + } + }) } catch (error) { functions.logger.error(`Error when trying hackerGetMentor: ${(error as Error).message} `) return res.status(500).json({ error: "An unexpected error occurred." });