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/ticket_controllers.ts b/functions/src/controllers/ticket_controllers.ts new file mode 100644 index 0000000..8f107fb --- /dev/null +++ b/functions/src/controllers/ticket_controllers.ts @@ -0,0 +1,126 @@ +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; + + 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) { + 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; + + 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) { + res.status(500).json({ error: (error as Error).message }); + } +}; diff --git a/functions/src/controllers/user_controller.ts b/functions/src/controllers/user_controller.ts new file mode 100644 index 0000000..a9fe628 --- /dev/null +++ b/functions/src/controllers/user_controller.ts @@ -0,0 +1,41 @@ +import { Request, Response } from "express"; +import { db } from "../config/firebase"; + +/** + * Fetch all users + */ +export const getUsers = async (req: Request, res: Response): Promise => { + try { + const snapshot = await db.collection("users").get(); + const users = snapshot.docs.map((doc) => ({ + id: doc.id, + ...doc.data(), + })); + res.json(users); + } catch (error) { + res.status(500).json({ error: (error as Error).message }); + } +}; + +export const getCurrentUser = async ( + req: Request, + res: Response +): Promise => { + try { + const userId = req.user?.uid; + if (!userId) { + res.status(401).json({ error: "Unauthorized" }); + return; + } + + const userDoc = await db.collection("users").doc(userId).get(); + if (!userDoc.exists) { + res.status(404).json({ error: "User not found" }); + return; + } + + res.json({ id: userDoc.id, ...userDoc.data() }); + } catch (error) { + res.status(500).json({ error: (error as Error).message }); + } +}; diff --git a/functions/src/controllers/users_controller.ts b/functions/src/controllers/users_controller.ts deleted file mode 100644 index 8169285..0000000 --- a/functions/src/controllers/users_controller.ts +++ /dev/null @@ -1,101 +0,0 @@ -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 - */ -export const getUsers = async (req: Request, res: Response): Promise => { - try { - const snapshot = await db.collection("users").get(); - const users = snapshot.docs.map((doc) => ({ - id: doc.id, - ...doc.data(), - })); - res.json(users); - } catch (error) { - res.status(500).json({ error: (error as Error).message }); - } -}; - -export const getCurrentUser = async ( - req: Request, - res: Response -): Promise => { - try { - const userId = req.user?.uid; - if (!userId) { - res.status(401).json({ error: "Unauthorized" }); - return; - } - - const userDoc = await db.collection("users").doc(userId).get(); - if (!userDoc.exists) { - res.status(404).json({ error: "User not found" }); - return; - } - - res.json({ id: userDoc.id, ...userDoc.data() }); - } 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/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..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 "./users"; +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; diff --git a/functions/src/routes/users.ts b/functions/src/routes/user.ts similarity index 68% rename from functions/src/routes/users.ts rename to functions/src/routes/user.ts index d022e70..be2fde4 100644 --- a/functions/src/routes/users.ts +++ b/functions/src/routes/user.ts @@ -1,9 +1,5 @@ import express, { Request, Response } from "express"; -import { - createUser, - getUsers, - getCurrentUser, -} from "../controllers/users_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;