From 46ee7341af2532be1f5f6b4dc1caefd8bdffe1c4 Mon Sep 17 00:00:00 2001 From: Hibatullah Fawwaz Hana Date: Fri, 18 Apr 2025 21:57:34 +0700 Subject: [PATCH 1/4] refactor: user --- functions/src/controllers/auth_controller.ts | 27 +++++++--------- ...users_controller.ts => user_controller.ts} | 0 functions/src/models/user.ts | 31 +++++++++++++++++++ functions/src/routes/index.ts | 2 +- functions/src/routes/{users.ts => user.ts} | 2 +- 5 files changed, 45 insertions(+), 17 deletions(-) rename functions/src/controllers/{users_controller.ts => user_controller.ts} (100%) create mode 100644 functions/src/models/user.ts rename functions/src/routes/{users.ts => user.ts} (92%) diff --git a/functions/src/controllers/auth_controller.ts b/functions/src/controllers/auth_controller.ts index c7950a2..b879571 100644 --- a/functions/src/controllers/auth_controller.ts +++ b/functions/src/controllers/auth_controller.ts @@ -4,6 +4,7 @@ import axios from "axios"; import validator from "validator"; import { FieldValue } from "firebase-admin/firestore"; import { convertResponseToSnakeCase } from "../utils/camel_case"; +import { User, formatUser } from "../models/user"; const validateEmailAndPassword = ( email: string, @@ -96,24 +97,20 @@ export const register = async (req: Request, res: Response): Promise => { await axios.post(url, { token: customToken, returnSecureToken: true }) ).data; - await db.collection("users").doc(user.uid).set({ - email: user.email, - first_name: user.displayName, - last_name: null, - date_of_birth: null, - education: null, - school: null, - grade: null, - year: null, - gender_identity: null, + const userData: User = formatUser({ + email: user.email ?? "", + firstName: user.displayName ?? "", status: "not applicable", - portfolio: null, - github: null, - linkedin: null, - admin: false, - created_at: FieldValue.serverTimestamp(), }); + await db + .collection("users") + .doc(user.uid) + .set({ + ...userData, + createdAt: FieldValue.serverTimestamp(), + }); + res.status(201).json( convertResponseToSnakeCase({ message: "Registration successful", diff --git a/functions/src/controllers/users_controller.ts b/functions/src/controllers/user_controller.ts similarity index 100% rename from functions/src/controllers/users_controller.ts rename to functions/src/controllers/user_controller.ts diff --git a/functions/src/models/user.ts b/functions/src/models/user.ts new file mode 100644 index 0000000..267e782 --- /dev/null +++ b/functions/src/models/user.ts @@ -0,0 +1,31 @@ +export interface User { + firstName: string; + lastName: string; + email: string; + dateOfBirth?: string; + school?: string; + grade?: number | null; + year?: number | null; + genderIdentity?: string; + status?: string; + portfolio?: string; + github?: string; + linkedin?: string; + admin?: boolean; +} + +export const formatUser = (data: Partial): User => ({ + firstName: data.firstName || "", + lastName: data.lastName || "", + email: data.email || "", + dateOfBirth: data.dateOfBirth || "", + school: data.school || "", + grade: data.grade || null, + year: data.year || null, + genderIdentity: data.genderIdentity || "", + status: data.status || "not applicable", + portfolio: data.portfolio || "", + github: data.github || "", + linkedin: data.linkedin || "", + admin: data.admin || false, +}); diff --git a/functions/src/routes/index.ts b/functions/src/routes/index.ts index acc8b70..4302a80 100644 --- a/functions/src/routes/index.ts +++ b/functions/src/routes/index.ts @@ -1,6 +1,6 @@ import express, { Router } from "express"; import authRoutes from "./auth"; -import userRoutes from "./users"; +import userRoutes from "./user"; const router: Router = express.Router(); diff --git a/functions/src/routes/users.ts b/functions/src/routes/user.ts similarity index 92% rename from functions/src/routes/users.ts rename to functions/src/routes/user.ts index d022e70..a83764d 100644 --- a/functions/src/routes/users.ts +++ b/functions/src/routes/user.ts @@ -3,7 +3,7 @@ import { createUser, getUsers, getCurrentUser, -} from "../controllers/users_controller"; +} from "../controllers/user_controller"; import { validateFirebaseIdToken } from "../middlewares/auth_middleware"; const router = express.Router(); From c693181249d679ed1e08e18f2c8cf27b748eaa45 Mon Sep 17 00:00:00 2001 From: Hibatullah Fawwaz Hana Date: Fri, 18 Apr 2025 22:01:15 +0700 Subject: [PATCH 2/4] refactor: user --- functions/src/controllers/user_controller.ts | 60 -------------------- functions/src/routes/user.ts | 7 +-- 2 files changed, 1 insertion(+), 66 deletions(-) diff --git a/functions/src/controllers/user_controller.ts b/functions/src/controllers/user_controller.ts index 8169285..a9fe628 100644 --- a/functions/src/controllers/user_controller.ts +++ b/functions/src/controllers/user_controller.ts @@ -1,66 +1,6 @@ import { Request, Response } from "express"; import { db } from "../config/firebase"; -interface User { - firstName: string; - lastName: string; - email: string; - dateOfBirth?: string; - school?: string; - grade?: number | null; - year?: number | null; - genderIdentity?: string; - status?: string; - portfolio?: string; - github?: string; - linkedin?: string; - admin?: boolean; -} - -/** - * Helper for standardizing user data - */ -const formatUser = (data: Partial): User => ({ - firstName: data.firstName || "", - lastName: data.lastName || "", - email: data.email || "", - dateOfBirth: data.dateOfBirth || "", - school: data.school || "", - grade: data.grade || null, - year: data.year || null, - genderIdentity: data.genderIdentity || "", - status: data.status || "", - portfolio: data.portfolio || "", - github: data.github || "", - linkedin: data.linkedin || "", - admin: data.admin || false, -}); - -/** - * Creates new user - */ -export const createUser = async ( - req: Request, - res: Response -): Promise => { - try { - const requiredFields: (keyof User)[] = ["email", "firstName", "lastName"]; - - for (const field of requiredFields) { - if (!req.body[field]) { - res.status(400).json({ error: `Missing required field: ${field}` }); - return; - } - } - - const userData: User = formatUser(req.body); - const userRef = await db.collection("users").add({ ...userData }); - res.json({ success: true, userId: userRef.id }); - } catch (error) { - res.status(500).json({ error: (error as Error).message }); - } -}; - /** * Fetch all users */ diff --git a/functions/src/routes/user.ts b/functions/src/routes/user.ts index a83764d..be2fde4 100644 --- a/functions/src/routes/user.ts +++ b/functions/src/routes/user.ts @@ -1,9 +1,5 @@ import express, { Request, Response } from "express"; -import { - createUser, - getUsers, - getCurrentUser, -} from "../controllers/user_controller"; +import { getUsers, getCurrentUser } from "../controllers/user_controller"; import { validateFirebaseIdToken } from "../middlewares/auth_middleware"; const router = express.Router(); @@ -11,7 +7,6 @@ const router = express.Router(); router.use(validateFirebaseIdToken); router.get("/", (req: Request, res: Response) => getUsers(req, res)); -router.post("/create", (req: Request, res: Response) => createUser(req, res)); router.get("/me", (req: Request, res: Response) => getCurrentUser(req, res)); export default router; From 871e9fe1b402c9d4e0eba9490d9bb80b14d1835a Mon Sep 17 00:00:00 2001 From: Hibatullah Fawwaz Hana Date: Sat, 19 Apr 2025 09:34:55 +0700 Subject: [PATCH 3/4] feat: ticket --- .../src/controllers/ticket_controllers.ts | 114 ++++++++++++++++++ functions/src/models/ticket.ts | 21 ++++ functions/src/routes/index.ts | 2 + functions/src/routes/ticket.ts | 21 ++++ 4 files changed, 158 insertions(+) create mode 100644 functions/src/controllers/ticket_controllers.ts create mode 100644 functions/src/models/ticket.ts create mode 100644 functions/src/routes/ticket.ts diff --git a/functions/src/controllers/ticket_controllers.ts b/functions/src/controllers/ticket_controllers.ts new file mode 100644 index 0000000..f9bebe0 --- /dev/null +++ b/functions/src/controllers/ticket_controllers.ts @@ -0,0 +1,114 @@ +import { Request, Response } from "express"; +import { db } from "../config/firebase"; +import { Ticket, formatTicket } from "../models/ticket"; + +/** + * Create a new ticket + */ +export const createTicket = async ( + req: Request, + res: Response +): Promise => { + try { + const data = req.body as Partial; + + if (!req.user || !req.user.uid) { + res.status(401).json({ error: "Unauthorized" }); + return; + } + + const ticketRef = await db.collection("tickets").add({ + topic: data.topic || "", + description: data.description || "", + location: data.location || "", + requestorId: req.user.uid, + tags: Array.isArray(data.tags) ? data.tags : [], + taken: false, + resolved: false, + }); + + res.status(201).json({ success: true, id: ticketRef.id }); + } catch (error) { + res.status(500).json({ error: (error as Error).message }); + } +}; + +/** + * Get all tickets + */ +export const getTickets = async ( + _req: Request, + res: Response +): Promise => { + try { + const snapshot = await db + .collection("tickets") + .where("resolved", "==", false) + .get(); + const tickets = snapshot.docs.map((doc) => + formatTicket({ id: doc.id, ...doc.data() }) + ); + res.status(200).json(tickets); + } catch (error) { + res.status(500).json({ error: (error as Error).message }); + } +}; + +/** + * Get a ticket by ID + */ +export const getTicketById = async ( + req: Request, + res: Response +): Promise => { + try { + const { id } = req.params; + const doc = await db.collection("tickets").doc(id).get(); + + if (!doc.exists) { + res.status(404).json({ error: "Ticket not found" }); + return; + } + + res.status(200).json(formatTicket({ id: doc.id, ...doc.data() })); + } catch (error) { + res.status(500).json({ error: (error as Error).message }); + } +}; + +/** + * Update a ticket + */ +export const updateTicket = async ( + req: Request, + res: Response +): Promise => { + try { + const { id } = req.params; + const data = req.body as Partial; + + await db.collection("tickets").doc(id).update(data); + + res.status(200).json({ success: true, message: "Ticket updated" }); + } catch (error) { + res.status(500).json({ error: (error as Error).message }); + } +}; + +/** + * Delete a ticket + */ +export const deleteTicket = async ( + req: Request, + res: Response +): Promise => { + try { + const { id } = req.params; + + await db.collection("tickets").doc(id).delete(); + + res.status(200).json({ success: true, message: "Ticket deleted" }); + } catch (error) { + res.status(500).json({ error: (error as Error).message }); + } +}; diff --git a/functions/src/models/ticket.ts b/functions/src/models/ticket.ts new file mode 100644 index 0000000..d529787 --- /dev/null +++ b/functions/src/models/ticket.ts @@ -0,0 +1,21 @@ +export interface Ticket { + id: string; + topic: string; + description: string; + location: string; + requestorId: string; + tags: string[]; + taken: boolean; + resolved: boolean; +} + +export const formatTicket = (data: Partial): Ticket => ({ + id: data.id || "", + topic: data.topic || "", + description: data.description || "", + location: data.location || "", + requestorId: data.requestorId || "", + tags: Array.isArray(data.tags) ? data.tags : [], + taken: data.taken ?? false, + resolved: data.resolved ?? false, +}); diff --git a/functions/src/routes/index.ts b/functions/src/routes/index.ts index 4302a80..4a636a2 100644 --- a/functions/src/routes/index.ts +++ b/functions/src/routes/index.ts @@ -1,10 +1,12 @@ import express, { Router } from "express"; import authRoutes from "./auth"; import userRoutes from "./user"; +import ticketRoutes from "./ticket"; const router: Router = express.Router(); router.use("/auth", authRoutes); router.use("/users", userRoutes); +router.use("/tickets", ticketRoutes); export default router; diff --git a/functions/src/routes/ticket.ts b/functions/src/routes/ticket.ts new file mode 100644 index 0000000..5043f92 --- /dev/null +++ b/functions/src/routes/ticket.ts @@ -0,0 +1,21 @@ +import express, { Request, Response } from "express"; +import { + getTickets, + createTicket, + getTicketById, + updateTicket, + deleteTicket, +} from "../controllers/ticket_controllers"; +import { validateFirebaseIdToken } from "../middlewares/auth_middleware"; + +const router = express.Router(); + +router.use(validateFirebaseIdToken); + +router.get("/", (req: Request, res: Response) => getTickets(req, res)); +router.get("/:id", (req: Request, res: Response) => getTicketById(req, res)); +router.post("/", (req: Request, res: Response) => createTicket(req, res)); +router.put("/:id", (req: Request, res: Response) => updateTicket(req, res)); +router.delete("/:id", (req: Request, res: Response) => deleteTicket(req, res)); + +export default router; From 7410cb7b4c327d2df7258787c4343e0e5dc385af Mon Sep 17 00:00:00 2001 From: Hibatullah Fawwaz Hana Date: Sat, 19 Apr 2025 09:40:51 +0700 Subject: [PATCH 4/4] fix: update & delete ticket validation --- functions/src/controllers/ticket_controllers.ts | 16 ++++++++++++++-- 1 file changed, 14 insertions(+), 2 deletions(-) diff --git a/functions/src/controllers/ticket_controllers.ts b/functions/src/controllers/ticket_controllers.ts index f9bebe0..8f107fb 100644 --- a/functions/src/controllers/ticket_controllers.ts +++ b/functions/src/controllers/ticket_controllers.ts @@ -87,7 +87,13 @@ export const updateTicket = async ( const { id } = req.params; const data = req.body as Partial; - await db.collection("tickets").doc(id).update(data); + const ticketDoc = db.collection("tickets").doc(id); + const ticketSnap = await ticketDoc.get(); + if (!ticketSnap.exists) { + res.status(404).json({ error: "Ticket not found" }); + return; + } + await ticketDoc.update(data); res.status(200).json({ success: true, message: "Ticket updated" }); } catch (error) { @@ -105,7 +111,13 @@ export const deleteTicket = async ( try { const { id } = req.params; - await db.collection("tickets").doc(id).delete(); + const ticketDoc = db.collection("tickets").doc(id); + const ticketSnap = await ticketDoc.get(); + if (!ticketSnap.exists) { + res.status(404).json({ error: "Ticket not found" }); + return; + } + await ticketDoc.delete(); res.status(200).json({ success: true, message: "Ticket deleted" }); } catch (error) {