From 58ab85281988f39617d249a9c95ebb013f01e367 Mon Sep 17 00:00:00 2001 From: rxmox Date: Sun, 25 Jan 2026 20:08:58 -0700 Subject: [PATCH 01/10] Add user and event lookup endpoints Implement three new GET endpoints: - GET /api/users/:userId - Get user by ID - GET /api/events/:eventId - Get event by ID - GET /api/users/:userId/events - Get all events a user has joined All endpoints follow existing patterns with proper error handling, validation, and response formats. --- .../src/controllers/event_controller.ts | 38 ++++++++++++++ .../src/controllers/user_controller.ts | 49 +++++++++++++++++++ shatter-backend/src/routes/event_routes.ts | 3 +- shatter-backend/src/routes/user_route.ts | 8 ++- 4 files changed, 96 insertions(+), 2 deletions(-) diff --git a/shatter-backend/src/controllers/event_controller.ts b/shatter-backend/src/controllers/event_controller.ts index aa54532..9d1468b 100644 --- a/shatter-backend/src/controllers/event_controller.ts +++ b/shatter-backend/src/controllers/event_controller.ts @@ -290,3 +290,41 @@ export async function joinEventAsGuest(req: Request, res: Response) { return res.status(500).json({ success: false, msg: "Internal error" }); } } + +/** + * GET /api/events/:eventId + * Get event details by event ID + * + * @param req.params.eventId - Event ID (required) + * + * @returns 200 with event details on success + * @returns 400 if eventId is missing + * @returns 404 if event is not found + */ +export async function getEventById(req: Request, res: Response) { + try { + const { eventId } = req.params; + + if (!eventId) { + return res + .status(400) + .json({ success: false, error: "eventId is required" }); + } + + const event = await Event.findById(eventId).populate( + "participantIds", + "name userId", + ); + + if (!event) { + return res.status(404).json({ success: false, error: "Event not found" }); + } + + res.status(200).json({ + success: true, + event, + }); + } catch (err: any) { + res.status(500).json({ success: false, error: err.message }); + } +} diff --git a/shatter-backend/src/controllers/user_controller.ts b/shatter-backend/src/controllers/user_controller.ts index 877b53a..e33c0d0 100644 --- a/shatter-backend/src/controllers/user_controller.ts +++ b/shatter-backend/src/controllers/user_controller.ts @@ -39,3 +39,52 @@ export const createUser = async (req: Request, res: Response) => { res.status(500).json({ error: "Failed to create user" }); } }; + +// controller: GET /api/users/:userId +// Get a specific user by ID +export const getUserById = async (req: Request, res: Response) => { + try { + const { userId } = req.params; + + if (!userId) { + return res.status(400).json({ success: false, error: "userId is required" }); + } + + const user = await User.findById(userId).select("-passwordHash"); + + if (!user) { + return res.status(404).json({ success: false, error: "User not found" }); + } + + res.status(200).json({ success: true, user }); + } catch (err: any) { + res.status(500).json({ success: false, error: err.message }); + } +}; + +// controller: GET /api/users/:userId/events +// Get all events that a user has joined +export const getUserEvents = async (req: Request, res: Response) => { + try { + const { userId } = req.params; + + if (!userId) { + return res.status(400).json({ success: false, error: "userId is required" }); + } + + const user = await User.findById(userId) + .populate("eventHistoryIds", "name description joinCode startDate endDate currentState") + .select("eventHistoryIds"); + + if (!user) { + return res.status(404).json({ success: false, error: "User not found" }); + } + + res.status(200).json({ + success: true, + events: user.eventHistoryIds, + }); + } catch (err: any) { + res.status(500).json({ success: false, error: err.message }); + } +}; diff --git a/shatter-backend/src/routes/event_routes.ts b/shatter-backend/src/routes/event_routes.ts index bc5fa58..761a120 100644 --- a/shatter-backend/src/routes/event_routes.ts +++ b/shatter-backend/src/routes/event_routes.ts @@ -1,5 +1,5 @@ import { Router } from 'express'; -import { createEvent, getEventByJoinCode, joinEventAsUser, joinEventAsGuest } from '../controllers/event_controller'; +import { createEvent, getEventByJoinCode, getEventById, joinEventAsUser, joinEventAsGuest } from '../controllers/event_controller'; import { authMiddleware } from '../middleware/auth_middleware'; const router = Router(); @@ -7,6 +7,7 @@ const router = Router(); router.post("/createEvent", authMiddleware, createEvent); router.get("/event/:joinCode", getEventByJoinCode); +router.get("/:eventId", getEventById); router.post("/:eventId/join/user", authMiddleware, joinEventAsUser); router.post("/:eventId/join/guest", joinEventAsGuest); diff --git a/shatter-backend/src/routes/user_route.ts b/shatter-backend/src/routes/user_route.ts index a3d9720..949a7a4 100644 --- a/shatter-backend/src/routes/user_route.ts +++ b/shatter-backend/src/routes/user_route.ts @@ -1,5 +1,5 @@ import { Router, Request, Response } from 'express'; -import { getUsers, createUser } from '../controllers/user_controller'; +import { getUsers, createUser, getUserById, getUserEvents } from '../controllers/user_controller'; import { authMiddleware } from '../middleware/auth_middleware'; import { User } from '../models/user_model'; @@ -22,4 +22,10 @@ router.get('/me', authMiddleware, async (req: Request, res: Response) => { } }); +// Get all events a user has joined - must come before /:userId to avoid route conflict +router.get('/:userId/events', getUserEvents); + +// Get user by ID +router.get('/:userId', getUserById); + export default router; From e429cd1f7aeeedc9bbfc3d4989cf94a508ccbc4b Mon Sep 17 00:00:00 2001 From: Le Nguyen Quang Minh <101281380+lnqminh3003@users.noreply.github.com> Date: Mon, 26 Jan 2026 12:24:40 -0700 Subject: [PATCH 02/10] Add authentication for get user data from userId and get events from userId endpoints --- shatter-backend/src/routes/user_route.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/shatter-backend/src/routes/user_route.ts b/shatter-backend/src/routes/user_route.ts index 949a7a4..d8df3e2 100644 --- a/shatter-backend/src/routes/user_route.ts +++ b/shatter-backend/src/routes/user_route.ts @@ -23,9 +23,9 @@ router.get('/me', authMiddleware, async (req: Request, res: Response) => { }); // Get all events a user has joined - must come before /:userId to avoid route conflict -router.get('/:userId/events', getUserEvents); +router.get('/:userId/events', authMiddleware, getUserEvents); // Get user by ID -router.get('/:userId', getUserById); +router.get('/:userId', authMiddleware, getUserById); export default router; From 9f64b449c33e0be785661f92bf440dac489f89c5 Mon Sep 17 00:00:00 2001 From: Inko-z Date: Thu, 29 Jan 2026 18:47:11 -0700 Subject: [PATCH 03/10] Functionality --- shatter-backend/src/app.ts | 2 + .../user_connections_controller.ts | 223 ++++++++++++++++++ .../src/models/user_connection_model.ts | 47 ++++ .../src/routes/user_connections_routes.ts | 32 +++ shatter-backend/src/utils/requests_utils.ts | 16 ++ 5 files changed, 320 insertions(+) create mode 100644 shatter-backend/src/controllers/user_connections_controller.ts create mode 100644 shatter-backend/src/models/user_connection_model.ts create mode 100644 shatter-backend/src/routes/user_connections_routes.ts create mode 100644 shatter-backend/src/utils/requests_utils.ts diff --git a/shatter-backend/src/app.ts b/shatter-backend/src/app.ts index 401d13a..5fc89c0 100644 --- a/shatter-backend/src/app.ts +++ b/shatter-backend/src/app.ts @@ -5,6 +5,7 @@ import userRoutes from './routes/user_route'; // these routes define how to hand import authRoutes from './routes/auth_routes'; import eventRoutes from './routes/event_routes'; import bingoRoutes from './routes/bingo_routes'; +import userConnectionRoutes from './routes/user_connections_routes'; const app = express(); @@ -43,5 +44,6 @@ app.use('/api/users', userRoutes); app.use('/api/auth', authRoutes); app.use('/api/events', eventRoutes); app.use('/api/bingo', bingoRoutes); +app.use('/api/userConnections', userConnectionRoutes); export default app; diff --git a/shatter-backend/src/controllers/user_connections_controller.ts b/shatter-backend/src/controllers/user_connections_controller.ts new file mode 100644 index 0000000..3e30e30 --- /dev/null +++ b/shatter-backend/src/controllers/user_connections_controller.ts @@ -0,0 +1,223 @@ +import { Request, Response } from "express"; +import { Types } from "mongoose"; + +import { check_req_fields } from "../utils/requests_utils"; +import { User } from "../models/user_model"; +import { UserConnection } from "../models/user_connection_model"; // <-- adjust path/name if needed + + + +/** + * POST /api/userConnections + * Create a new UserConnection document. + * The server generates the UserConnection `_id` automatically (pre-save hook). + * + * @param req.body._eventId - MongoDB ObjectId of the event (required) + * @param req.body.primaryUserId - MongoDB ObjectId of the primary user (required) + * @param req.body.secondaryUserId - MongoDB ObjectId of the secondary user (required) + * @param req.body.description - Optional description for the connection, can be the bingo question the users connected with for example (optional) + * + * @returns 201 - Created UserConnection document + * @returns 400 - Missing required fields or invalid ObjectId format + * @returns 404 - Primary user or secondary user not found + * @returns 500 - Internal server error + */ + +export async function createUserConnection(req: Request, res: Response) { + try { + const requiredFields = ["_eventId", "primaryUserId", "secondaryUserId"]; + if (!check_req_fields(req, requiredFields)) { + return res.status(400).json({ error: "Missing required fields" }); + } + + const { _eventId, primaryUserId, secondaryUserId, description } = req.body; + + // Validate ObjectId format before hitting the DB + const idsToValidate = { _eventId, primaryUserId, secondaryUserId }; + for (const [key, value] of Object.entries(idsToValidate)) { + if (!Types.ObjectId.isValid(value)) { + return res.status(400).json({ error: `Invalid ${key}` }); + } + } + + // Check that both users exist + const [primaryExists, secondaryExists] = await Promise.all([ + User.exists({ _id: primaryUserId }), + User.exists({ _id: secondaryUserId }), + ]); + + if (!primaryExists) { + return res.status(404).json({ error: "Primary user not found" }); + } + if (!secondaryExists) { + return res.status(404).json({ error: "Secondary user not found" }); + } + + // Create the connection (schema pre-save will generate _id if missing) + const newConnection = await UserConnection.create({ + _eventId, + primaryUserId, + secondaryUserId, + description, + }); + + return res.status(201).json(newConnection); + } catch (error) { + return res.status(500).json({ error: "Internal server error" }); + } +} + +/** + * POST /api/userConnections/by-emails + * Create a new UserConnection by providing user emails. + * Looks up users by email, then saves the connection using their MongoDB ObjectIds. + * The server generates the UserConnection `_id` automatically (pre-save hook). + * + * @param req.body._eventId - MongoDB ObjectId of the event (required) + * @param req.body.primaryUserEmail - Email of the primary user (required) + * @param req.body.secondaryUserEmail - Email of the secondary user (required) + * @param req.body.description - Optional description for the connection (optional) + * + * @returns 201 - Created UserConnection document + * @returns 400 - Missing required fields, invalid ObjectId, invalid emails, or same email provided twice + * @returns 404 - Primary user or secondary user not found + * @returns 500 - Internal server error + */ +export async function createUserConnectionByEmails(req: Request, res: Response) { + try { + const requiredFields = ["_eventId", "primaryUserEmail", "secondaryUserEmail"]; + if (!check_req_fields(req, requiredFields)) { + return res.status(400).json({ error: "Missing required fields" }); + } + + const { _eventId, primaryUserEmail, secondaryUserEmail, description } = req.body; + + // Validate event id format + if (!Types.ObjectId.isValid(_eventId)) { + return res.status(400).json({ error: "Invalid _eventId" }); + } + + // Normalize emails (your schema lowercases, so match that) + const primaryEmail = String(primaryUserEmail).trim().toLowerCase(); + const secondaryEmail = String(secondaryUserEmail).trim().toLowerCase(); + + // Basic email check (optional but helpful) + const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]{2,}$/; + if (!emailRegex.test(primaryEmail)) { + return res.status(400).json({ error: "Invalid primaryUserEmail" }); + } + if (!emailRegex.test(secondaryEmail)) { + return res.status(400).json({ error: "Invalid secondaryUserEmail" }); + } + + if (primaryEmail === secondaryEmail) { + return res.status(400).json({ error: "primaryUserEmail and secondaryUserEmail must be different" }); + } + + // Lookup users by email (only need _id) + const [primaryUser, secondaryUser] = await Promise.all([ + User.findOne({ email: primaryEmail }).select("_id"), + User.findOne({ email: secondaryEmail }).select("_id"), + ]); + + if (!primaryUser) { + return res.status(404).json({ error: "Primary user not found" }); + } + if (!secondaryUser) { + return res.status(404).json({ error: "Secondary user not found" }); + } + + // Create connection storing ObjectIds (not emails) + const newConnection = await UserConnection.create({ + _eventId, + primaryUserId: primaryUser._id, + secondaryUserId: secondaryUser._id, + description, + }); + + return res.status(201).json(newConnection); + } catch (error) { + return res.status(500).json({ error: "Internal server error" }); + } +} + +/** + * DELETE /api/userConnections/delete + * + * Deletes a user connection only if it belongs to the given event. + * + * @param req.body.eventId - MongoDB ObjectId of the event (required) + * @param req.body.connectionId - UserConnection string _id (required) + * + * @returns 200 - Deleted connection + * @returns 400 - Missing/invalid body params + * @returns 404 - Connection not found for this event + * @returns 500 - Internal server error + */ +export async function deleteUserConnection(req: Request, res: Response) { + try { + const { eventId, connectionId } = req.body; + + if (!eventId || !Types.ObjectId.isValid(eventId)) { + return res.status(400).json({ error: "Invalid eventId" }); + } + + if (!connectionId || typeof connectionId !== "string") { + return res.status(400).json({ error: "Invalid connectionId" }); + } + + const deleted = await UserConnection.findOneAndDelete({ + _id: connectionId, + _eventId: eventId, + }); + + if (!deleted) { + return res + .status(404) + .json({ error: "UserConnection not found for this event" }); + } + + return res.status(200).json({ + message: "UserConnection deleted successfully", + deletedConnection: deleted, + }); + } catch (error) { + return res.status(500).json({ error: "Internal server error" }); + } +} + +/** + * PUT /api/userConnections/getByUserAndEvent + * + * Returns all UserConnections for an event where the given user is either: + * - primaryUserId OR + * - secondaryUserId + * + * @param req.body.eventId - MongoDB ObjectId of the event (required) + * @param req.body.userId - MongoDB ObjectId of the user (required) + * + * @returns 200 - Array of matching UserConnections + * @returns 400 - Missing/invalid body params + * @returns 500 - Internal server error + */ +export async function getConnectionsByUserAndEvent(req: Request, res: Response) { + try { + const { eventId, userId } = req.body; + + if (!eventId || !Types.ObjectId.isValid(eventId)) { + return res.status(400).json({ error: "Invalid eventId" }); + } + if (!userId || !Types.ObjectId.isValid(userId)) { + return res.status(400).json({ error: "Invalid userId" }); + } + + const connections = await UserConnection.find({ + _eventId: eventId, + $or: [{ primaryUserId: userId }, { secondaryUserId: userId }], + }); + + return res.status(200).json(connections); + } catch (error) { + return res.status(500).json({ error: "Internal server error" }); + } +} \ No newline at end of file diff --git a/shatter-backend/src/models/user_connection_model.ts b/shatter-backend/src/models/user_connection_model.ts new file mode 100644 index 0000000..6237640 --- /dev/null +++ b/shatter-backend/src/models/user_connection_model.ts @@ -0,0 +1,47 @@ +import { Schema, model, Types, Document } from "mongoose"; + + +export interface UserConnection extends Document { + _id: string; + _eventId: Types.ObjectId; + primaryUserId: Types.ObjectId; + secondaryUserId: Types.ObjectId; + description?: string; + + +} + +const userConnectionSchema = new Schema( + { + + _id: { type: String }, + _eventId: { + type: Schema.Types.ObjectId, + ref: "Event", + required: true, + }, + primaryUserId: { + type: Schema.Types.ObjectId, + ref: "User", + required: true, + }, + secondaryUserId: { + type: Schema.Types.ObjectId, + ref: "User", + required: true, + }, + description: { type: String }, + }, + { + versionKey: false, + } +); + +userConnectionSchema.pre("save", function (next) { + if (!this._id) { + this._id = `userConnection_${Math.random().toString(36).slice(2, 10)}`; + } + next(); +}); + +export const UserConnection = model("UserConnection", userConnectionSchema); diff --git a/shatter-backend/src/routes/user_connections_routes.ts b/shatter-backend/src/routes/user_connections_routes.ts new file mode 100644 index 0000000..ef4ca39 --- /dev/null +++ b/shatter-backend/src/routes/user_connections_routes.ts @@ -0,0 +1,32 @@ +// routes/user_connections_routes.ts +import { Router } from "express"; +import { + createUserConnection, + createUserConnectionByEmails, + deleteUserConnection, + getConnectionsByUserAndEvent, +} from "../controllers/user_connections_controller"; + +const router = Router(); + +/** + * Base path (mounted in app): /api/userConnections +*/ + +// Create connection by user ObjectIds +// POST /api/userConnections +router.post("/", createUserConnection); + +// Create connection by user emails (controller converts to ObjectIds) +// POST /api/userConnections/by-emails +router.post("/by-emails", createUserConnectionByEmails); + +// Delete connection (eventId + connectionId in request body) +// DELETE /api/userConnections/delete +router.delete("/delete", deleteUserConnection); + +// Get all connections for (eventId + userId) where user is primary or secondary +// PUT /api/userConnections/getByUserAndEvent +router.put("/getByUserAndEvent", getConnectionsByUserAndEvent); + +export default router; \ No newline at end of file diff --git a/shatter-backend/src/utils/requests_utils.ts b/shatter-backend/src/utils/requests_utils.ts new file mode 100644 index 0000000..7eb8258 --- /dev/null +++ b/shatter-backend/src/utils/requests_utils.ts @@ -0,0 +1,16 @@ +import { Request } from "express"; + +/** + * Checks if all required fields are present in the request body. + * @param req - Express request object + * @param requiredFields - Array of required field names + * @returns boolean indicating if all required fields are present + */ +export function check_req_fields(req: Request, requiredFields: string[]): boolean { + for (const field of requiredFields) { + if (!req.body[field]) { + return false; + } + } + return true; +} \ No newline at end of file From 36109d11904de964d15b48041ead4b0be85fa159 Mon Sep 17 00:00:00 2001 From: Inko-z Date: Thu, 5 Feb 2026 17:47:13 -0700 Subject: [PATCH 04/10] Fixed Controller logic. No duplicate creation --- shatter-backend/src/app.ts | 2 +- .../user_connections_controller.ts | 59 +++++++++++-------- 2 files changed, 36 insertions(+), 25 deletions(-) diff --git a/shatter-backend/src/app.ts b/shatter-backend/src/app.ts index 5fc89c0..293b6f4 100644 --- a/shatter-backend/src/app.ts +++ b/shatter-backend/src/app.ts @@ -46,4 +46,4 @@ app.use('/api/events', eventRoutes); app.use('/api/bingo', bingoRoutes); app.use('/api/userConnections', userConnectionRoutes); -export default app; +export default app; \ No newline at end of file diff --git a/shatter-backend/src/controllers/user_connections_controller.ts b/shatter-backend/src/controllers/user_connections_controller.ts index 3e30e30..a8478df 100644 --- a/shatter-backend/src/controllers/user_connections_controller.ts +++ b/shatter-backend/src/controllers/user_connections_controller.ts @@ -3,7 +3,8 @@ import { Types } from "mongoose"; import { check_req_fields } from "../utils/requests_utils"; import { User } from "../models/user_model"; -import { UserConnection } from "../models/user_connection_model"; // <-- adjust path/name if needed +import { UserConnection } from "../models/user_connection_model"; +import { Event } from "../models/event_model"; @@ -46,14 +47,23 @@ export async function createUserConnection(req: Request, res: Response) { User.exists({ _id: secondaryUserId }), ]); - if (!primaryExists) { - return res.status(404).json({ error: "Primary user not found" }); - } - if (!secondaryExists) { - return res.status(404).json({ error: "Secondary user not found" }); + if (!primaryExists) return res.status(404).json({ error: "Primary user not found" }); + if (!secondaryExists) return res.status(404).json({ error: "Secondary user not found" }); + + // ✅ NEW: prevent duplicates with exact same (_eventId, primaryUserId, secondaryUserId) + const existing = await UserConnection.findOne({ + _eventId, + primaryUserId, + secondaryUserId, + }); + + if (existing) { + return res.status(409).json({ + error: "UserConnection already exists for this event and users", + existingConnection: existing, + }); } - // Create the connection (schema pre-save will generate _id if missing) const newConnection = await UserConnection.create({ _eventId, primaryUserId, @@ -92,42 +102,43 @@ export async function createUserConnectionByEmails(req: Request, res: Response) const { _eventId, primaryUserEmail, secondaryUserEmail, description } = req.body; - // Validate event id format if (!Types.ObjectId.isValid(_eventId)) { return res.status(400).json({ error: "Invalid _eventId" }); } - // Normalize emails (your schema lowercases, so match that) const primaryEmail = String(primaryUserEmail).trim().toLowerCase(); const secondaryEmail = String(secondaryUserEmail).trim().toLowerCase(); - // Basic email check (optional but helpful) const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]{2,}$/; - if (!emailRegex.test(primaryEmail)) { - return res.status(400).json({ error: "Invalid primaryUserEmail" }); - } - if (!emailRegex.test(secondaryEmail)) { - return res.status(400).json({ error: "Invalid secondaryUserEmail" }); - } + if (!emailRegex.test(primaryEmail)) return res.status(400).json({ error: "Invalid primaryUserEmail" }); + if (!emailRegex.test(secondaryEmail)) return res.status(400).json({ error: "Invalid secondaryUserEmail" }); if (primaryEmail === secondaryEmail) { return res.status(400).json({ error: "primaryUserEmail and secondaryUserEmail must be different" }); } - // Lookup users by email (only need _id) const [primaryUser, secondaryUser] = await Promise.all([ User.findOne({ email: primaryEmail }).select("_id"), User.findOne({ email: secondaryEmail }).select("_id"), ]); - if (!primaryUser) { - return res.status(404).json({ error: "Primary user not found" }); - } - if (!secondaryUser) { - return res.status(404).json({ error: "Secondary user not found" }); + if (!primaryUser) return res.status(404).json({ error: "Primary user not found" }); + if (!secondaryUser) return res.status(404).json({ error: "Secondary user not found" }); + + // ✅ NEW: prevent duplicates with exact same (_eventId, primaryUserId, secondaryUserId) + const existing = await UserConnection.findOne({ + _eventId, + primaryUserId: primaryUser._id, + secondaryUserId: secondaryUser._id, + }); + + if (existing) { + return res.status(409).json({ + error: "UserConnection already exists for this event and users", + existingConnection: existing, + }); } - // Create connection storing ObjectIds (not emails) const newConnection = await UserConnection.create({ _eventId, primaryUserId: primaryUser._id, @@ -159,7 +170,7 @@ export async function deleteUserConnection(req: Request, res: Response) { const { eventId, connectionId } = req.body; if (!eventId || !Types.ObjectId.isValid(eventId)) { - return res.status(400).json({ error: "Invalid eventId" }); + return res.status(400).json({ error: "Invalid eventId hmmm" }); } if (!connectionId || typeof connectionId !== "string") { From e7442ab739c4fbbe273101a9369edb6a32576eab Mon Sep 17 00:00:00 2001 From: Inko-z Date: Thu, 5 Feb 2026 18:42:53 -0700 Subject: [PATCH 05/10] Added get cconnections by email route --- .../user_connections_controller.ts | 41 +++++++++++++++++++ .../src/routes/user_connections_routes.ts | 5 +++ 2 files changed, 46 insertions(+) diff --git a/shatter-backend/src/controllers/user_connections_controller.ts b/shatter-backend/src/controllers/user_connections_controller.ts index a8478df..f1af965 100644 --- a/shatter-backend/src/controllers/user_connections_controller.ts +++ b/shatter-backend/src/controllers/user_connections_controller.ts @@ -231,4 +231,45 @@ export async function getConnectionsByUserAndEvent(req: Request, res: Response) } catch (error) { return res.status(500).json({ error: "Internal server error" }); } +} + + +/** + * PUT /api/userConnections/getByUserEmailAndEvent + * + * Returns all UserConnections for an event where the given user is either: + * - primaryUserEmail OR + * - secondaryUserEmail + * + * @param req.body.eventId - MongoDB ObjectId of the event (required) + * @param req.body.userEmail - Email of the user (required) + * + * @returns 200 - Array of matching UserConnections + * @returns 400 - Missing/invalid body params + * @returns 500 - Internal server error + */ +export async function getConnectionsByUserEmailAndEvent(req: Request, res: Response) { + try { + const { eventId, userEmail } = req.body; + + if (!eventId || !Types.ObjectId.isValid(eventId)) { + return res.status(400).json({ error: "Invalid eventId" }); + } + + const user_Id= await User.findOne({email: userEmail}, '_id'); + + if (!user_Id) { + return res.status(400).json({ error: "Invalid userEmail" }); + } + + + const connections = await UserConnection.find({ + _eventId: eventId, + $or: [{ primaryUserId: user_Id._id }, { secondaryUserId: user_Id._id }], + }); + return res.status(200).json(connections); + + } catch (error) { + return res.status(500).json({ error: "Internal server error" }); + } } \ No newline at end of file diff --git a/shatter-backend/src/routes/user_connections_routes.ts b/shatter-backend/src/routes/user_connections_routes.ts index ef4ca39..ad3e63d 100644 --- a/shatter-backend/src/routes/user_connections_routes.ts +++ b/shatter-backend/src/routes/user_connections_routes.ts @@ -5,6 +5,7 @@ import { createUserConnectionByEmails, deleteUserConnection, getConnectionsByUserAndEvent, + getConnectionsByUserEmailAndEvent, } from "../controllers/user_connections_controller"; const router = Router(); @@ -29,4 +30,8 @@ router.delete("/delete", deleteUserConnection); // PUT /api/userConnections/getByUserAndEvent router.put("/getByUserAndEvent", getConnectionsByUserAndEvent); +// Get all connections for (eventId + userEmail) where user is primary or secondary +// PUT /api/userConnections/getByUserEmailAndEvent +router.put("/getByUserEmailAndEvent", getConnectionsByUserEmailAndEvent); + export default router; \ No newline at end of file From 8730ab722ab87e00f4d38472e6a62297b3055ff9 Mon Sep 17 00:00:00 2001 From: Le Nguyen Quang Minh <101281380+lnqminh3003@users.noreply.github.com> Date: Fri, 6 Feb 2026 21:44:43 -0700 Subject: [PATCH 06/10] Update route for bingo game --- shatter-backend/src/controllers/bingo_controller.ts | 6 +----- shatter-backend/src/routes/bingo_routes.ts | 8 ++++---- 2 files changed, 5 insertions(+), 9 deletions(-) diff --git a/shatter-backend/src/controllers/bingo_controller.ts b/shatter-backend/src/controllers/bingo_controller.ts index acf7661..be75831 100644 --- a/shatter-backend/src/controllers/bingo_controller.ts +++ b/shatter-backend/src/controllers/bingo_controller.ts @@ -72,13 +72,9 @@ export async function createBingo(req: Request, res: Response) { } } - -/** - * @param req.body.id - Bingo _id (string) OR Event _id (ObjectId string) (required) - */ export async function getBingo(req: Request, res: Response) { try { - const { id } = req.body; + const { id } = req.params; if (!id) { return res.status(400).json({ diff --git a/shatter-backend/src/routes/bingo_routes.ts b/shatter-backend/src/routes/bingo_routes.ts index 2e9238f..9973f67 100644 --- a/shatter-backend/src/routes/bingo_routes.ts +++ b/shatter-backend/src/routes/bingo_routes.ts @@ -1,16 +1,16 @@ import { Router } from 'express'; import { createBingo, getBingo, updateBingo} from '../controllers/bingo_controller'; +import { authMiddleware } from '../middleware/auth_middleware'; const router = Router(); -// POST /api/bingo/createEvent - create new event -router.post('/createBingo', createBingo); +router.post('/createBingo', authMiddleware, createBingo); // POST /api/bingo/getBingo - get bingo details -router.post('/getBingo', getBingo); +router.get('/getBingo/:id', getBingo); // POST /api/bingo/updateBingo - update bingo details -router.put("/updateBingo", updateBingo); +router.put("/updateBingo", authMiddleware, updateBingo); export default router; From 93fbf7645ff954c97f4ae676e992a3dd6bd66cd9 Mon Sep 17 00:00:00 2001 From: Le Nguyen Quang Minh <101281380+lnqminh3003@users.noreply.github.com> Date: Sun, 8 Feb 2026 11:00:42 -0700 Subject: [PATCH 07/10] Revert "Update route for bingo game" This reverts commit 8730ab722ab87e00f4d38472e6a62297b3055ff9. --- shatter-backend/src/controllers/bingo_controller.ts | 6 +++++- shatter-backend/src/routes/bingo_routes.ts | 8 ++++---- 2 files changed, 9 insertions(+), 5 deletions(-) diff --git a/shatter-backend/src/controllers/bingo_controller.ts b/shatter-backend/src/controllers/bingo_controller.ts index be75831..acf7661 100644 --- a/shatter-backend/src/controllers/bingo_controller.ts +++ b/shatter-backend/src/controllers/bingo_controller.ts @@ -72,9 +72,13 @@ export async function createBingo(req: Request, res: Response) { } } + +/** + * @param req.body.id - Bingo _id (string) OR Event _id (ObjectId string) (required) + */ export async function getBingo(req: Request, res: Response) { try { - const { id } = req.params; + const { id } = req.body; if (!id) { return res.status(400).json({ diff --git a/shatter-backend/src/routes/bingo_routes.ts b/shatter-backend/src/routes/bingo_routes.ts index 9973f67..2e9238f 100644 --- a/shatter-backend/src/routes/bingo_routes.ts +++ b/shatter-backend/src/routes/bingo_routes.ts @@ -1,16 +1,16 @@ import { Router } from 'express'; import { createBingo, getBingo, updateBingo} from '../controllers/bingo_controller'; -import { authMiddleware } from '../middleware/auth_middleware'; const router = Router(); -router.post('/createBingo', authMiddleware, createBingo); +// POST /api/bingo/createEvent - create new event +router.post('/createBingo', createBingo); // POST /api/bingo/getBingo - get bingo details -router.get('/getBingo/:id', getBingo); +router.post('/getBingo', getBingo); // POST /api/bingo/updateBingo - update bingo details -router.put("/updateBingo", authMiddleware, updateBingo); +router.put("/updateBingo", updateBingo); export default router; From 07c68f58b9b8ed227674ffd16e7f675d45fc7f14 Mon Sep 17 00:00:00 2001 From: Rodolfo Date: Sun, 22 Feb 2026 20:41:16 -0700 Subject: [PATCH 08/10] Updating UserConnections to now be participant connenctions. Needs to be tested, missing latest .env --- shatter-backend/src/app.ts | 6 +- .../participant_connections_controller.ts | 318 ++++++++++++++++++ .../user_connections_controller.ts | 275 --------------- .../models/participant_connection_model.ts | 46 +++ .../src/models/user_connection_model.ts | 47 --- .../routes/participant_connections_routes.ts | 59 ++++ .../src/routes/user_connections_routes.ts | 37 -- 7 files changed, 426 insertions(+), 362 deletions(-) create mode 100644 shatter-backend/src/controllers/participant_connections_controller.ts delete mode 100644 shatter-backend/src/controllers/user_connections_controller.ts create mode 100644 shatter-backend/src/models/participant_connection_model.ts delete mode 100644 shatter-backend/src/models/user_connection_model.ts create mode 100644 shatter-backend/src/routes/participant_connections_routes.ts delete mode 100644 shatter-backend/src/routes/user_connections_routes.ts diff --git a/shatter-backend/src/app.ts b/shatter-backend/src/app.ts index 293b6f4..7f5f7ea 100644 --- a/shatter-backend/src/app.ts +++ b/shatter-backend/src/app.ts @@ -1,11 +1,11 @@ import express from "express"; import cors from "cors"; -import userRoutes from './routes/user_route'; // these routes define how to handle requests to /api/users +import userRoutes from './routes/user_route'; import authRoutes from './routes/auth_routes'; import eventRoutes from './routes/event_routes'; import bingoRoutes from './routes/bingo_routes'; -import userConnectionRoutes from './routes/user_connections_routes'; +import participantConnectionRoutes from "./routes/participant_connections_routes"; const app = express(); @@ -44,6 +44,6 @@ app.use('/api/users', userRoutes); app.use('/api/auth', authRoutes); app.use('/api/events', eventRoutes); app.use('/api/bingo', bingoRoutes); -app.use('/api/userConnections', userConnectionRoutes); +app.use("/api/participantConnections", participantConnectionRoutes); export default app; \ No newline at end of file diff --git a/shatter-backend/src/controllers/participant_connections_controller.ts b/shatter-backend/src/controllers/participant_connections_controller.ts new file mode 100644 index 0000000..ebd786c --- /dev/null +++ b/shatter-backend/src/controllers/participant_connections_controller.ts @@ -0,0 +1,318 @@ +// controllers/participant_connections_controller.ts + +import { Request, Response } from "express"; +import { Types } from "mongoose"; + +import { check_req_fields } from "../utils/requests_utils"; +import { User } from "../models/user_model"; +import { Participant } from "../models/participant_model"; +import { ParticipantConnection } from "../models/participant_connection_model"; + +/** + * POST /api/participantConnections + * Create a new ParticipantConnection document. + * The server generates the ParticipantConnection `_id` automatically (pre-save hook). + * + * @param req.body._eventId - MongoDB ObjectId of the event (required) + * @param req.body.primaryParticipantId - MongoDB ObjectId of the primary participant (required) + * @param req.body.secondaryParticipantId - MongoDB ObjectId of the secondary participant (required) + * @param req.body.description - Optional description for the connection, can be the bingo question the participants connected with (optional) + * + * Behavior notes: + * - Validates ObjectId format before hitting the DB + * - Ensures BOTH participants exist AND belong to the given event + * - Prevents duplicates with exact same (_eventId, primaryParticipantId, secondaryParticipantId) + * + * @returns 201 - Created ParticipantConnection document + * @returns 400 - Missing required fields or invalid ObjectId format + * @returns 404 - Primary participant or secondary participant not found for this event + * @returns 409 - ParticipantConnection already exists for this event and participants + * @returns 500 - Internal server error + */ +export async function createParticipantConnection(req: Request, res: Response) { + try { + const requiredFields = ["_eventId", "primaryParticipantId", "secondaryParticipantId"]; + if (!check_req_fields(req, requiredFields)) { + return res.status(400).json({ error: "Missing required fields" }); + } + + const { _eventId, primaryParticipantId, secondaryParticipantId, description } = req.body; + + // Validate ObjectId format before hitting the DB + const idsToValidate = { _eventId, primaryParticipantId, secondaryParticipantId }; + for (const [key, value] of Object.entries(idsToValidate)) { + if (!Types.ObjectId.isValid(value)) { + return res.status(400).json({ error: `Invalid ${key}` }); + } + } + + // Ensure both participants exist AND belong to the event + const [primaryParticipant, secondaryParticipant] = await Promise.all([ + Participant.findOne({ _id: primaryParticipantId, eventId: _eventId }).select("_id"), + Participant.findOne({ _id: secondaryParticipantId, eventId: _eventId }).select("_id"), + ]); + + if (!primaryParticipant) { + return res.status(404).json({ error: "Primary participant not found for this event" }); + } + if (!secondaryParticipant) { + return res.status(404).json({ error: "Secondary participant not found for this event" }); + } + + // Prevent duplicates with exact same (_eventId, primaryParticipantId, secondaryParticipantId) + const existing = await ParticipantConnection.findOne({ + _eventId, + primaryParticipantId, + secondaryParticipantId, + }); + + if (existing) { + return res.status(409).json({ + error: "ParticipantConnection already exists for this event and participants", + existingConnection: existing, + }); + } + + const newConnection = await ParticipantConnection.create({ + _eventId, + primaryParticipantId, + secondaryParticipantId, + description, + }); + + return res.status(201).json(newConnection); + } catch (_error) { + return res.status(500).json({ error: "Internal server error" }); + } +} + +/** + * POST /api/participantConnections/by-emails + * Create a new ParticipantConnection by providing USER emails. + * + * Looks up users by email, then finds their Participant records for the given event, + * then saves the connection using the participants' MongoDB ObjectIds. + * The server generates the ParticipantConnection `_id` automatically (pre-save hook). + * + * @param req.body._eventId - MongoDB ObjectId of the event (required) + * @param req.body.primaryUserEmail - Email of the primary user (required) + * @param req.body.secondaryUserEmail - Email of the secondary user (required) + * @param req.body.description - Optional description for the connection (optional) + * + * Behavior notes: + * - Validates _eventId ObjectId format + * - Validates emails + prevents same email provided twice + * - Maps email -> User -> Participant (for that event) + * - Prevents duplicates with exact same (_eventId, primaryParticipantId, secondaryParticipantId) + * + * @returns 201 - Created ParticipantConnection document + * @returns 400 - Missing required fields, invalid ObjectId, invalid emails, or same email provided twice + * @returns 404 - Primary/secondary user not found OR participant not found for this event + * @returns 409 - ParticipantConnection already exists for this event and participants + * @returns 500 - Internal server error + */ +export async function createParticipantConnectionByEmails(req: Request, res: Response) { + try { + const requiredFields = ["_eventId", "primaryUserEmail", "secondaryUserEmail"]; + if (!check_req_fields(req, requiredFields)) { + return res.status(400).json({ error: "Missing required fields" }); + } + + const { _eventId, primaryUserEmail, secondaryUserEmail, description } = req.body; + + if (!Types.ObjectId.isValid(_eventId)) { + return res.status(400).json({ error: "Invalid _eventId" }); + } + + const primaryEmail = String(primaryUserEmail).trim().toLowerCase(); + const secondaryEmail = String(secondaryUserEmail).trim().toLowerCase(); + + const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]{2,}$/; + if (!emailRegex.test(primaryEmail)) { + return res.status(400).json({ error: "Invalid primaryUserEmail" }); + } + if (!emailRegex.test(secondaryEmail)) { + return res.status(400).json({ error: "Invalid secondaryUserEmail" }); + } + + if (primaryEmail === secondaryEmail) { + return res.status(400).json({ error: "primaryUserEmail and secondaryUserEmail must be different" }); + } + + // Find users by email + const [primaryUser, secondaryUser] = await Promise.all([ + User.findOne({ email: primaryEmail }).select("_id"), + User.findOne({ email: secondaryEmail }).select("_id"), + ]); + + if (!primaryUser) return res.status(404).json({ error: "Primary user not found" }); + if (!secondaryUser) return res.status(404).json({ error: "Secondary user not found" }); + + // Map User -> Participant (for the event) + const [primaryParticipant, secondaryParticipant] = await Promise.all([ + Participant.findOne({ eventId: _eventId, userId: primaryUser._id }).select("_id"), + Participant.findOne({ eventId: _eventId, userId: secondaryUser._id }).select("_id"), + ]); + + if (!primaryParticipant) { + return res.status(404).json({ error: "Primary participant not found for this event (by user email)" }); + } + if (!secondaryParticipant) { + return res.status(404).json({ error: "Secondary participant not found for this event (by user email)" }); + } + + // Prevent duplicates with exact same (_eventId, primaryParticipantId, secondaryParticipantId) + const existing = await ParticipantConnection.findOne({ + _eventId, + primaryParticipantId: primaryParticipant._id, + secondaryParticipantId: secondaryParticipant._id, + }); + + if (existing) { + return res.status(409).json({ + error: "ParticipantConnection already exists for this event and participants", + existingConnection: existing, + }); + } + + const newConnection = await ParticipantConnection.create({ + _eventId, + primaryParticipantId: primaryParticipant._id, + secondaryParticipantId: secondaryParticipant._id, + description, + }); + + return res.status(201).json(newConnection); + } catch (_error) { + return res.status(500).json({ error: "Internal server error" }); + } +} + +/** + * DELETE /api/participantConnections/delete + * + * Deletes a participant connection only if it belongs to the given event. + * + * @param req.body.eventId - MongoDB ObjectId of the event (required) + * @param req.body.connectionId - ParticipantConnection string _id (required) + * + * @returns 200 - Deleted connection + * @returns 400 - Missing/invalid body params + * @returns 404 - ParticipantConnection not found for this event + * @returns 500 - Internal server error + */ +export async function deleteParticipantConnection(req: Request, res: Response) { + try { + const { eventId, connectionId } = req.body; + + if (!eventId || !Types.ObjectId.isValid(eventId)) { + return res.status(400).json({ error: "Invalid eventId" }); + } + + if (!connectionId || typeof connectionId !== "string") { + return res.status(400).json({ error: "Invalid connectionId" }); + } + + const deleted = await ParticipantConnection.findOneAndDelete({ + _id: connectionId, + _eventId: eventId, + }); + + if (!deleted) { + return res.status(404).json({ error: "ParticipantConnection not found for this event" }); + } + + return res.status(200).json({ + message: "ParticipantConnection deleted successfully", + deletedConnection: deleted, + }); + } catch (_error) { + return res.status(500).json({ error: "Internal server error" }); + } +} + +/** + * PUT /api/participantConnections/getByParticipantAndEvent + * + * Returns all ParticipantConnections for an event where the given participant is either: + * - primaryParticipantId OR + * - secondaryParticipantId + * + * @param req.body.eventId - MongoDB ObjectId of the event (required) + * @param req.body.participantId - MongoDB ObjectId of the participant (required) + * + * @returns 200 - Array of matching ParticipantConnections + * @returns 400 - Missing/invalid body params + * @returns 500 - Internal server error + */ +export async function getConnectionsByParticipantAndEvent(req: Request, res: Response) { + try { + const { eventId, participantId } = req.body; + + if (!eventId || !Types.ObjectId.isValid(eventId)) { + return res.status(400).json({ error: "Invalid eventId" }); + } + if (!participantId || !Types.ObjectId.isValid(participantId)) { + return res.status(400).json({ error: "Invalid participantId" }); + } + + const connections = await ParticipantConnection.find({ + _eventId: eventId, + $or: [{ primaryParticipantId: participantId }, { secondaryParticipantId: participantId }], + }); + + return res.status(200).json(connections); + } catch (_error) { + return res.status(500).json({ error: "Internal server error" }); + } +} + +/** + * PUT /api/participantConnections/getByUserEmailAndEvent + * + * Returns all ParticipantConnections for an event where the given user's PARTICIPANT is either: + * - primaryParticipantId OR + * - secondaryParticipantId + * + * @param req.body.eventId - MongoDB ObjectId of the event (required) + * @param req.body.userEmail - Email of the user (required) + * + * Behavior notes: + * - Maps userEmail -> User -> Participant (for that event) + * - Then returns ParticipantConnections where that participant appears + * + * @returns 200 - Array of matching ParticipantConnections + * @returns 400 - Missing/invalid body params or invalid userEmail (no matching user) + * @returns 404 - Participant not found for this event (even though user exists) + * @returns 500 - Internal server error + */ +export async function getConnectionsByUserEmailAndEvent(req: Request, res: Response) { + try { + const { eventId, userEmail } = req.body; + + if (!eventId || !Types.ObjectId.isValid(eventId)) { + return res.status(400).json({ error: "Invalid eventId" }); + } + + const email = String(userEmail).trim().toLowerCase(); + const user = await User.findOne({ email }).select("_id"); + + if (!user) { + return res.status(400).json({ error: "Invalid userEmail" }); + } + + const participant = await Participant.findOne({ eventId, userId: user._id }).select("_id"); + if (!participant) { + return res.status(404).json({ error: "Participant not found for this event (by user email)" }); + } + + const connections = await ParticipantConnection.find({ + _eventId: eventId, + $or: [{ primaryParticipantId: participant._id }, { secondaryParticipantId: participant._id }], + }); + + return res.status(200).json(connections); + } catch (_error) { + return res.status(500).json({ error: "Internal server error" }); + } +} \ No newline at end of file diff --git a/shatter-backend/src/controllers/user_connections_controller.ts b/shatter-backend/src/controllers/user_connections_controller.ts deleted file mode 100644 index f1af965..0000000 --- a/shatter-backend/src/controllers/user_connections_controller.ts +++ /dev/null @@ -1,275 +0,0 @@ -import { Request, Response } from "express"; -import { Types } from "mongoose"; - -import { check_req_fields } from "../utils/requests_utils"; -import { User } from "../models/user_model"; -import { UserConnection } from "../models/user_connection_model"; -import { Event } from "../models/event_model"; - - - -/** - * POST /api/userConnections - * Create a new UserConnection document. - * The server generates the UserConnection `_id` automatically (pre-save hook). - * - * @param req.body._eventId - MongoDB ObjectId of the event (required) - * @param req.body.primaryUserId - MongoDB ObjectId of the primary user (required) - * @param req.body.secondaryUserId - MongoDB ObjectId of the secondary user (required) - * @param req.body.description - Optional description for the connection, can be the bingo question the users connected with for example (optional) - * - * @returns 201 - Created UserConnection document - * @returns 400 - Missing required fields or invalid ObjectId format - * @returns 404 - Primary user or secondary user not found - * @returns 500 - Internal server error - */ - -export async function createUserConnection(req: Request, res: Response) { - try { - const requiredFields = ["_eventId", "primaryUserId", "secondaryUserId"]; - if (!check_req_fields(req, requiredFields)) { - return res.status(400).json({ error: "Missing required fields" }); - } - - const { _eventId, primaryUserId, secondaryUserId, description } = req.body; - - // Validate ObjectId format before hitting the DB - const idsToValidate = { _eventId, primaryUserId, secondaryUserId }; - for (const [key, value] of Object.entries(idsToValidate)) { - if (!Types.ObjectId.isValid(value)) { - return res.status(400).json({ error: `Invalid ${key}` }); - } - } - - // Check that both users exist - const [primaryExists, secondaryExists] = await Promise.all([ - User.exists({ _id: primaryUserId }), - User.exists({ _id: secondaryUserId }), - ]); - - if (!primaryExists) return res.status(404).json({ error: "Primary user not found" }); - if (!secondaryExists) return res.status(404).json({ error: "Secondary user not found" }); - - // ✅ NEW: prevent duplicates with exact same (_eventId, primaryUserId, secondaryUserId) - const existing = await UserConnection.findOne({ - _eventId, - primaryUserId, - secondaryUserId, - }); - - if (existing) { - return res.status(409).json({ - error: "UserConnection already exists for this event and users", - existingConnection: existing, - }); - } - - const newConnection = await UserConnection.create({ - _eventId, - primaryUserId, - secondaryUserId, - description, - }); - - return res.status(201).json(newConnection); - } catch (error) { - return res.status(500).json({ error: "Internal server error" }); - } -} - -/** - * POST /api/userConnections/by-emails - * Create a new UserConnection by providing user emails. - * Looks up users by email, then saves the connection using their MongoDB ObjectIds. - * The server generates the UserConnection `_id` automatically (pre-save hook). - * - * @param req.body._eventId - MongoDB ObjectId of the event (required) - * @param req.body.primaryUserEmail - Email of the primary user (required) - * @param req.body.secondaryUserEmail - Email of the secondary user (required) - * @param req.body.description - Optional description for the connection (optional) - * - * @returns 201 - Created UserConnection document - * @returns 400 - Missing required fields, invalid ObjectId, invalid emails, or same email provided twice - * @returns 404 - Primary user or secondary user not found - * @returns 500 - Internal server error - */ -export async function createUserConnectionByEmails(req: Request, res: Response) { - try { - const requiredFields = ["_eventId", "primaryUserEmail", "secondaryUserEmail"]; - if (!check_req_fields(req, requiredFields)) { - return res.status(400).json({ error: "Missing required fields" }); - } - - const { _eventId, primaryUserEmail, secondaryUserEmail, description } = req.body; - - if (!Types.ObjectId.isValid(_eventId)) { - return res.status(400).json({ error: "Invalid _eventId" }); - } - - const primaryEmail = String(primaryUserEmail).trim().toLowerCase(); - const secondaryEmail = String(secondaryUserEmail).trim().toLowerCase(); - - const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]{2,}$/; - if (!emailRegex.test(primaryEmail)) return res.status(400).json({ error: "Invalid primaryUserEmail" }); - if (!emailRegex.test(secondaryEmail)) return res.status(400).json({ error: "Invalid secondaryUserEmail" }); - - if (primaryEmail === secondaryEmail) { - return res.status(400).json({ error: "primaryUserEmail and secondaryUserEmail must be different" }); - } - - const [primaryUser, secondaryUser] = await Promise.all([ - User.findOne({ email: primaryEmail }).select("_id"), - User.findOne({ email: secondaryEmail }).select("_id"), - ]); - - if (!primaryUser) return res.status(404).json({ error: "Primary user not found" }); - if (!secondaryUser) return res.status(404).json({ error: "Secondary user not found" }); - - // ✅ NEW: prevent duplicates with exact same (_eventId, primaryUserId, secondaryUserId) - const existing = await UserConnection.findOne({ - _eventId, - primaryUserId: primaryUser._id, - secondaryUserId: secondaryUser._id, - }); - - if (existing) { - return res.status(409).json({ - error: "UserConnection already exists for this event and users", - existingConnection: existing, - }); - } - - const newConnection = await UserConnection.create({ - _eventId, - primaryUserId: primaryUser._id, - secondaryUserId: secondaryUser._id, - description, - }); - - return res.status(201).json(newConnection); - } catch (error) { - return res.status(500).json({ error: "Internal server error" }); - } -} - -/** - * DELETE /api/userConnections/delete - * - * Deletes a user connection only if it belongs to the given event. - * - * @param req.body.eventId - MongoDB ObjectId of the event (required) - * @param req.body.connectionId - UserConnection string _id (required) - * - * @returns 200 - Deleted connection - * @returns 400 - Missing/invalid body params - * @returns 404 - Connection not found for this event - * @returns 500 - Internal server error - */ -export async function deleteUserConnection(req: Request, res: Response) { - try { - const { eventId, connectionId } = req.body; - - if (!eventId || !Types.ObjectId.isValid(eventId)) { - return res.status(400).json({ error: "Invalid eventId hmmm" }); - } - - if (!connectionId || typeof connectionId !== "string") { - return res.status(400).json({ error: "Invalid connectionId" }); - } - - const deleted = await UserConnection.findOneAndDelete({ - _id: connectionId, - _eventId: eventId, - }); - - if (!deleted) { - return res - .status(404) - .json({ error: "UserConnection not found for this event" }); - } - - return res.status(200).json({ - message: "UserConnection deleted successfully", - deletedConnection: deleted, - }); - } catch (error) { - return res.status(500).json({ error: "Internal server error" }); - } -} - -/** - * PUT /api/userConnections/getByUserAndEvent - * - * Returns all UserConnections for an event where the given user is either: - * - primaryUserId OR - * - secondaryUserId - * - * @param req.body.eventId - MongoDB ObjectId of the event (required) - * @param req.body.userId - MongoDB ObjectId of the user (required) - * - * @returns 200 - Array of matching UserConnections - * @returns 400 - Missing/invalid body params - * @returns 500 - Internal server error - */ -export async function getConnectionsByUserAndEvent(req: Request, res: Response) { - try { - const { eventId, userId } = req.body; - - if (!eventId || !Types.ObjectId.isValid(eventId)) { - return res.status(400).json({ error: "Invalid eventId" }); - } - if (!userId || !Types.ObjectId.isValid(userId)) { - return res.status(400).json({ error: "Invalid userId" }); - } - - const connections = await UserConnection.find({ - _eventId: eventId, - $or: [{ primaryUserId: userId }, { secondaryUserId: userId }], - }); - - return res.status(200).json(connections); - } catch (error) { - return res.status(500).json({ error: "Internal server error" }); - } -} - - -/** - * PUT /api/userConnections/getByUserEmailAndEvent - * - * Returns all UserConnections for an event where the given user is either: - * - primaryUserEmail OR - * - secondaryUserEmail - * - * @param req.body.eventId - MongoDB ObjectId of the event (required) - * @param req.body.userEmail - Email of the user (required) - * - * @returns 200 - Array of matching UserConnections - * @returns 400 - Missing/invalid body params - * @returns 500 - Internal server error - */ -export async function getConnectionsByUserEmailAndEvent(req: Request, res: Response) { - try { - const { eventId, userEmail } = req.body; - - if (!eventId || !Types.ObjectId.isValid(eventId)) { - return res.status(400).json({ error: "Invalid eventId" }); - } - - const user_Id= await User.findOne({email: userEmail}, '_id'); - - if (!user_Id) { - return res.status(400).json({ error: "Invalid userEmail" }); - } - - - const connections = await UserConnection.find({ - _eventId: eventId, - $or: [{ primaryUserId: user_Id._id }, { secondaryUserId: user_Id._id }], - }); - return res.status(200).json(connections); - - } catch (error) { - return res.status(500).json({ error: "Internal server error" }); - } -} \ No newline at end of file diff --git a/shatter-backend/src/models/participant_connection_model.ts b/shatter-backend/src/models/participant_connection_model.ts new file mode 100644 index 0000000..5f48106 --- /dev/null +++ b/shatter-backend/src/models/participant_connection_model.ts @@ -0,0 +1,46 @@ +import { Schema, model, Types, Document } from "mongoose"; + +export interface ParticipantConnection extends Document { + _id: string; + _eventId: Types.ObjectId; + primaryParticipantId: Types.ObjectId; + secondaryParticipantId: Types.ObjectId; + description?: string; +} + +const participantConnectionSchema = new Schema( + { + _id: { type: String }, + _eventId: { + type: Schema.Types.ObjectId, + ref: "Event", + required: true, + }, + primaryParticipantId: { + type: Schema.Types.ObjectId, + ref: "Participant", + required: true, + }, + secondaryParticipantId: { + type: Schema.Types.ObjectId, + ref: "Participant", + required: true, + }, + description: { type: String }, + }, + { + versionKey: false, + } +); + +participantConnectionSchema.pre("save", function (next) { + if (!this._id) { + this._id = `participantConnection_${Math.random().toString(36).slice(2, 10)}`; + } + next(); +}); + +export const ParticipantConnection = model( + "ParticipantConnection", + participantConnectionSchema +); \ No newline at end of file diff --git a/shatter-backend/src/models/user_connection_model.ts b/shatter-backend/src/models/user_connection_model.ts deleted file mode 100644 index 6237640..0000000 --- a/shatter-backend/src/models/user_connection_model.ts +++ /dev/null @@ -1,47 +0,0 @@ -import { Schema, model, Types, Document } from "mongoose"; - - -export interface UserConnection extends Document { - _id: string; - _eventId: Types.ObjectId; - primaryUserId: Types.ObjectId; - secondaryUserId: Types.ObjectId; - description?: string; - - -} - -const userConnectionSchema = new Schema( - { - - _id: { type: String }, - _eventId: { - type: Schema.Types.ObjectId, - ref: "Event", - required: true, - }, - primaryUserId: { - type: Schema.Types.ObjectId, - ref: "User", - required: true, - }, - secondaryUserId: { - type: Schema.Types.ObjectId, - ref: "User", - required: true, - }, - description: { type: String }, - }, - { - versionKey: false, - } -); - -userConnectionSchema.pre("save", function (next) { - if (!this._id) { - this._id = `userConnection_${Math.random().toString(36).slice(2, 10)}`; - } - next(); -}); - -export const UserConnection = model("UserConnection", userConnectionSchema); diff --git a/shatter-backend/src/routes/participant_connections_routes.ts b/shatter-backend/src/routes/participant_connections_routes.ts new file mode 100644 index 0000000..fa0690f --- /dev/null +++ b/shatter-backend/src/routes/participant_connections_routes.ts @@ -0,0 +1,59 @@ +// routes/participant_connections_routes.ts + +import { Router } from "express"; +import { + createParticipantConnection, + createParticipantConnectionByEmails, + deleteParticipantConnection, + getConnectionsByParticipantAndEvent, + getConnectionsByUserEmailAndEvent, +} from "../controllers/participant_connections_controller"; + +const router = Router(); + +/** + * Base path (mounted in app): /api/participantConnections + * + * Routes: + * - POST /api/participantConnections + * Create connection by participant ObjectIds + * + * - POST /api/participantConnections/by-emails + * Create connection by user emails (controller maps email -> User -> Participant -> ObjectIds) + * + * - DELETE /api/participantConnections/delete + * Delete connection (eventId + connectionId in request body) + * + * - PUT /api/participantConnections/getByParticipantAndEvent + * Get all connections for (eventId + participantId) where participant is primary or secondary + * + * - PUT /api/participantConnections/getByUserEmailAndEvent + * Get all connections for (eventId + userEmail) where the user's participant is primary or secondary + */ + + + + + + +// Create connection by participant ObjectIds +// POST /api/participantConnections +router.post("/", createParticipantConnection); + +// Create connection by user emails (controller converts to participant ObjectIds) +// POST /api/participantConnections/by-emails +router.post("/by-emails", createParticipantConnectionByEmails); + +// Delete connection (eventId + connectionId in request body) +// DELETE /api/participantConnections/delete +router.delete("/delete", deleteParticipantConnection); + +// Get all connections for (eventId + participantId) where participant is primary or secondary +// PUT /api/participantConnections/getByParticipantAndEvent +router.put("/getByParticipantAndEvent", getConnectionsByParticipantAndEvent); + +// Get all connections for (eventId + userEmail) where user's participant is primary or secondary +// PUT /api/participantConnections/getByUserEmailAndEvent +router.put("/getByUserEmailAndEvent", getConnectionsByUserEmailAndEvent); + +export default router; \ No newline at end of file diff --git a/shatter-backend/src/routes/user_connections_routes.ts b/shatter-backend/src/routes/user_connections_routes.ts deleted file mode 100644 index ad3e63d..0000000 --- a/shatter-backend/src/routes/user_connections_routes.ts +++ /dev/null @@ -1,37 +0,0 @@ -// routes/user_connections_routes.ts -import { Router } from "express"; -import { - createUserConnection, - createUserConnectionByEmails, - deleteUserConnection, - getConnectionsByUserAndEvent, - getConnectionsByUserEmailAndEvent, -} from "../controllers/user_connections_controller"; - -const router = Router(); - -/** - * Base path (mounted in app): /api/userConnections -*/ - -// Create connection by user ObjectIds -// POST /api/userConnections -router.post("/", createUserConnection); - -// Create connection by user emails (controller converts to ObjectIds) -// POST /api/userConnections/by-emails -router.post("/by-emails", createUserConnectionByEmails); - -// Delete connection (eventId + connectionId in request body) -// DELETE /api/userConnections/delete -router.delete("/delete", deleteUserConnection); - -// Get all connections for (eventId + userId) where user is primary or secondary -// PUT /api/userConnections/getByUserAndEvent -router.put("/getByUserAndEvent", getConnectionsByUserAndEvent); - -// Get all connections for (eventId + userEmail) where user is primary or secondary -// PUT /api/userConnections/getByUserEmailAndEvent -router.put("/getByUserEmailAndEvent", getConnectionsByUserEmailAndEvent); - -export default router; \ No newline at end of file From 9791aa61d6f3751477c7e40b1d5dcc61b450b38b Mon Sep 17 00:00:00 2001 From: Le Nguyen Quang Minh <101281380+lnqminh3003@users.noreply.github.com> Date: Sat, 28 Feb 2026 15:51:35 -0700 Subject: [PATCH 09/10] add authMiddleware --- .../src/routes/participant_connections_routes.ts | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/shatter-backend/src/routes/participant_connections_routes.ts b/shatter-backend/src/routes/participant_connections_routes.ts index fa0690f..c38250d 100644 --- a/shatter-backend/src/routes/participant_connections_routes.ts +++ b/shatter-backend/src/routes/participant_connections_routes.ts @@ -8,6 +8,7 @@ import { getConnectionsByParticipantAndEvent, getConnectionsByUserEmailAndEvent, } from "../controllers/participant_connections_controller"; +import { authMiddleware } from "../middleware/auth_middleware"; const router = Router(); @@ -38,22 +39,22 @@ const router = Router(); // Create connection by participant ObjectIds // POST /api/participantConnections -router.post("/", createParticipantConnection); +router.post("/", authMiddleware, createParticipantConnection); // Create connection by user emails (controller converts to participant ObjectIds) // POST /api/participantConnections/by-emails -router.post("/by-emails", createParticipantConnectionByEmails); +router.post("/by-emails", authMiddleware, createParticipantConnectionByEmails); // Delete connection (eventId + connectionId in request body) // DELETE /api/participantConnections/delete -router.delete("/delete", deleteParticipantConnection); +router.delete("/delete", authMiddleware, deleteParticipantConnection); // Get all connections for (eventId + participantId) where participant is primary or secondary // PUT /api/participantConnections/getByParticipantAndEvent -router.put("/getByParticipantAndEvent", getConnectionsByParticipantAndEvent); +router.put("/getByParticipantAndEvent", authMiddleware, getConnectionsByParticipantAndEvent); // Get all connections for (eventId + userEmail) where user's participant is primary or secondary // PUT /api/participantConnections/getByUserEmailAndEvent -router.put("/getByUserEmailAndEvent", getConnectionsByUserEmailAndEvent); +router.put("/getByUserEmailAndEvent", authMiddleware, getConnectionsByUserEmailAndEvent); export default router; \ No newline at end of file From 38cdd89dd2799bffc510fa2b051ddd3eff7e31ff Mon Sep 17 00:00:00 2001 From: Le Nguyen Quang Minh <101281380+lnqminh3003@users.noreply.github.com> Date: Sat, 28 Feb 2026 16:24:01 -0700 Subject: [PATCH 10/10] Change route method and use query parameters --- .../participant_connections_controller.ts | 61 ++++++------------- .../routes/participant_connections_routes.ts | 8 +-- 2 files changed, 24 insertions(+), 45 deletions(-) diff --git a/shatter-backend/src/controllers/participant_connections_controller.ts b/shatter-backend/src/controllers/participant_connections_controller.ts index ebd786c..d206b22 100644 --- a/shatter-backend/src/controllers/participant_connections_controller.ts +++ b/shatter-backend/src/controllers/participant_connections_controller.ts @@ -231,34 +231,24 @@ export async function deleteParticipantConnection(req: Request, res: Response) { } } -/** - * PUT /api/participantConnections/getByParticipantAndEvent - * - * Returns all ParticipantConnections for an event where the given participant is either: - * - primaryParticipantId OR - * - secondaryParticipantId - * - * @param req.body.eventId - MongoDB ObjectId of the event (required) - * @param req.body.participantId - MongoDB ObjectId of the participant (required) - * - * @returns 200 - Array of matching ParticipantConnections - * @returns 400 - Missing/invalid body params - * @returns 500 - Internal server error - */ export async function getConnectionsByParticipantAndEvent(req: Request, res: Response) { try { - const { eventId, participantId } = req.body; + const { eventId, participantId } = req.query; - if (!eventId || !Types.ObjectId.isValid(eventId)) { + if (!eventId || typeof eventId !== "string" || !Types.ObjectId.isValid(eventId)) { return res.status(400).json({ error: "Invalid eventId" }); } - if (!participantId || !Types.ObjectId.isValid(participantId)) { + + if (!participantId || typeof participantId !== "string" || !Types.ObjectId.isValid(participantId)) { return res.status(400).json({ error: "Invalid participantId" }); } const connections = await ParticipantConnection.find({ _eventId: eventId, - $or: [{ primaryParticipantId: participantId }, { secondaryParticipantId: participantId }], + $or: [ + { primaryParticipantId: participantId }, + { secondaryParticipantId: participantId }, + ], }); return res.status(200).json(connections); @@ -267,34 +257,19 @@ export async function getConnectionsByParticipantAndEvent(req: Request, res: Res } } -/** - * PUT /api/participantConnections/getByUserEmailAndEvent - * - * Returns all ParticipantConnections for an event where the given user's PARTICIPANT is either: - * - primaryParticipantId OR - * - secondaryParticipantId - * - * @param req.body.eventId - MongoDB ObjectId of the event (required) - * @param req.body.userEmail - Email of the user (required) - * - * Behavior notes: - * - Maps userEmail -> User -> Participant (for that event) - * - Then returns ParticipantConnections where that participant appears - * - * @returns 200 - Array of matching ParticipantConnections - * @returns 400 - Missing/invalid body params or invalid userEmail (no matching user) - * @returns 404 - Participant not found for this event (even though user exists) - * @returns 500 - Internal server error - */ export async function getConnectionsByUserEmailAndEvent(req: Request, res: Response) { try { - const { eventId, userEmail } = req.body; + const { eventId, userEmail } = req.query; - if (!eventId || !Types.ObjectId.isValid(eventId)) { + if (!eventId || typeof eventId !== "string" || !Types.ObjectId.isValid(eventId)) { return res.status(400).json({ error: "Invalid eventId" }); } - const email = String(userEmail).trim().toLowerCase(); + if (!userEmail || typeof userEmail !== "string") { + return res.status(400).json({ error: "Invalid userEmail" }); + } + + const email = userEmail.trim().toLowerCase(); const user = await User.findOne({ email }).select("_id"); if (!user) { @@ -302,13 +277,17 @@ export async function getConnectionsByUserEmailAndEvent(req: Request, res: Respo } const participant = await Participant.findOne({ eventId, userId: user._id }).select("_id"); + if (!participant) { return res.status(404).json({ error: "Participant not found for this event (by user email)" }); } const connections = await ParticipantConnection.find({ _eventId: eventId, - $or: [{ primaryParticipantId: participant._id }, { secondaryParticipantId: participant._id }], + $or: [ + { primaryParticipantId: participant._id }, + { secondaryParticipantId: participant._id } + ], }); return res.status(200).json(connections); diff --git a/shatter-backend/src/routes/participant_connections_routes.ts b/shatter-backend/src/routes/participant_connections_routes.ts index c38250d..383168e 100644 --- a/shatter-backend/src/routes/participant_connections_routes.ts +++ b/shatter-backend/src/routes/participant_connections_routes.ts @@ -50,11 +50,11 @@ router.post("/by-emails", authMiddleware, createParticipantConnectionByEmails); router.delete("/delete", authMiddleware, deleteParticipantConnection); // Get all connections for (eventId + participantId) where participant is primary or secondary -// PUT /api/participantConnections/getByParticipantAndEvent -router.put("/getByParticipantAndEvent", authMiddleware, getConnectionsByParticipantAndEvent); +// GET /api/participantConnections/getByParticipantAndEvent +router.get("/getByParticipantAndEvent", authMiddleware, getConnectionsByParticipantAndEvent); // Get all connections for (eventId + userEmail) where user's participant is primary or secondary -// PUT /api/participantConnections/getByUserEmailAndEvent -router.put("/getByUserEmailAndEvent", authMiddleware, getConnectionsByUserEmailAndEvent); +// GET /api/participantConnections/getByUserEmailAndEvent +router.get("/getByUserEmailAndEvent", authMiddleware, getConnectionsByUserEmailAndEvent); export default router; \ No newline at end of file