Skip to content
Merged
312 changes: 304 additions & 8 deletions shatter-backend/package-lock.json

Large diffs are not rendered by default.

3 changes: 3 additions & 0 deletions shatter-backend/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -14,10 +14,12 @@
"description": "",
"dependencies": {
"bcryptjs": "^3.0.3",
"cors": "^2.8.5",
"dotenv": "^17.2.3",
"express": "^5.1.0",
"jsonwebtoken": "^9.0.2",
"mongoose": "^8.19.2",
"socket.io": "^4.8.1",
"zod": "^4.1.12"
},
"devDependencies": {
Expand All @@ -26,6 +28,7 @@
"@types/express": "^5.0.5",
"@types/jsonwebtoken": "^9.0.10",
"@types/node": "^24.9.2",
"@types/socket.io": "^3.0.1",
"eslint": "^9.38.0",
"globals": "^16.4.0",
"jiti": "^2.6.1",
Expand Down
12 changes: 12 additions & 0 deletions shatter-backend/src/app.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,6 @@
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 authRoutes from './routes/auth_routes';
import eventRoutes from './routes/event_routes';
Expand All @@ -7,6 +9,16 @@ const app = express();

app.use(express.json());

app.use(cors({
origin: "http://localhost:3000",
credentials: true,
}));

app.use((req, _res, next) => {
req.io = app.get('socketio');
next();
});

app.get('/', (_req, res) => {
res.send('Hello');
});
Expand Down
12 changes: 8 additions & 4 deletions shatter-backend/src/controllers/auth_controller.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,11 +9,11 @@ const EMAIL_REGEX = /^[^\s@]+@[^\s@]+\.[^\s@]{2,}$/;
/**
* POST /api/auth/signup
* Create new user account
*
*
* @param req.body.name - User's display name
* @param req.body.email - User's email
* @param req.body.password - User's plain text password
* @returns 201 with userId on success
* @returns 201 with userId and JWT token on success
*/
export const signup = async (req: Request, res: Response) => {
try {
Expand Down Expand Up @@ -67,10 +67,14 @@ export const signup = async (req: Request, res: Response) => {
passwordHash
});

// return success
// generate JWT token for the new user
const token = generateToken(newUser._id.toString());

// return success with token
res.status(201).json({
message: 'User created successfully',
userId: newUser._id
userId: newUser._id,
token
});

} catch (err: any) {
Expand Down
265 changes: 250 additions & 15 deletions shatter-backend/src/controllers/event_controller.ts
Original file line number Diff line number Diff line change
@@ -1,32 +1,67 @@
import { Request, Response } from "express";
import { Event } from "../models/event_model";
import "../models/participant_model";

import {generateEventId, generateJoinCode} from "../utils/event_utils";
import "../models/participant_model";

import { generateJoinCode } from "../utils/event_utils";
import { Participant } from "../models/participant_model";
import { User } from "../models/user_model";
import { Types } from "mongoose";

/**
* POST /api/events/createEvent
* Create a new event
*
* @param req.body.name - Event name (required)
* @param req.body.description - Event description
* @param req.body.startDate - Event start date
* @param req.body.endDate - Event end date
* @param req.body.maxParticipant - Maximum number of participants
* @param req.body.currentState - Current state of the event
* @param req.user.userId - Authenticated user ID (from access token)
*
* @returns 201 with created event on success
* @returns 400 if required fields are missing
* @returns 404 if creator user is not found
*/
export async function createEvent(req: Request, res: Response) {
try {
const { name, description, startDate, endDate, maxParticipant, currentState, createdBy } = req.body;
const {
name,
description,
startDate,
endDate,
maxParticipant,
currentState,
} = req.body;

const createdBy = req.user!.userId;

if (!name) {
return res
.status(400)
.json({ success: false, error: "Event name is required" });
}

if (!createdBy) {
return res.status(400).json({ success: false, error: "createdBy email is required" });
const user = await User.findById(createdBy).select("_id");
if (!user) {
return res.status(404).json({
success: false,
msg: "User not found",
});
}

const eventId = generateEventId();
const joinCode = generateJoinCode();

const event = new Event({
eventId,
name,
description,
joinCode,
startDate,
endDate,
maxParticipant,
participants: [],
participantIds: [],
currentState,
createdBy, // required email field
createdBy, // user id
});

const savedEvent = await event.save();
Expand All @@ -37,17 +72,28 @@ export async function createEvent(req: Request, res: Response) {
}
}


/**
* GET /api/events/event/:joinCode
* Get event details by join code
*
* @param req.params.joinCode - Unique join code of the event (required)
*
* @returns 200 with event details on success
* @returns 400 if joinCode is missing
* @returns 404 if event is not found
*/
export async function getEventByJoinCode(req: Request, res: Response) {
try {
const { joinCode } = req.params;

if (!joinCode) {
return res.status(400).json({ success: false, error: "joinCode is required" });
return res
.status(400)
.json({ success: false, error: "joinCode is required" });
}

// Find event by joinCode and populate participants
const event = await Event.findOne({ joinCode }).populate("participants");
// const event = await Event.findOne({ joinCode }).populate("participantIds");
const event = await Event.findOne({ joinCode });

if (!event) {
return res.status(404).json({ success: false, error: "Event not found" });
Expand All @@ -60,4 +106,193 @@ export async function getEventByJoinCode(req: Request, res: Response) {
} catch (err: any) {
res.status(500).json({ success: false, error: err.message });
}
}
}

/**
* POST /api/events/:eventId/join/user
* Join an event as a registered user
*
* @param req.params.eventId - Event ID to join (required)
* @param req.body.userId - User ID joining the event (required)
* @param req.body.name - Display name of the participant (required)
*
* @returns 200 with participant info on success
* @returns 400 if required fields are missing or event is full
* @returns 404 if user or event is not found
* @returns 409 if user already joined the event
*/
export async function joinEventAsUser(req: Request, res: Response) {
try {
const { name, userId } = req.body;
const { eventId } = req.params;

if (!userId || !name || !eventId)
return res.status(400).json({
success: false,
msg: "Missing fields: userId, name, and eventId are required",
});

const user = await User.findById(userId).select("_id");
if (!user) {
return res.status(404).json({
success: false,
msg: "User not found",
});
}

const event = await Event.findById(eventId);
if (!event)
return res.status(404).json({ success: false, msg: "Event not found" });

if (event.participantIds.length >= event.maxParticipant)
return res.status(400).json({ success: false, msg: "Event is full" });

let participant = await Participant.findOne({
userId,
eventId,
});

if (participant) {
return res
.status(409)
.json({ success: false, msg: "User already joined" });
}

participant = await Participant.create({
userId,
name,
eventId,
});

const participantId = participant._id as Types.ObjectId;

const eventUpdate = await Event.updateOne(
{ _id: eventId },
{ $addToSet: { participantIds: participantId } }
);

if (eventUpdate.modifiedCount === 0) {
return res
.status(400)
.json({ success: false, msg: "Already joined this event" });
}

// Add event to user history
await User.updateOne(
{ _id: userId },
{ $addToSet: { eventHistoryIds: eventId } }
);

console.log("Room socket:", eventId);
console.log("Participant data:", { participantId, name });

if (!req.io) {
console.error("ERROR: req.io is undefined!");
} else {
const room = req.io.to(eventId);
console.log("Room object:", room);

room.emit("participant-joined", {
participantId,
name,
});

console.log("Socket event emitted successfully");
}

return res.json({
success: true,
participant,
});
} catch (e: any) {
if (e.code === 11000) {
return res.status(409).json({
success: false,
msg: "This name is already taken in this event",
});
}
console.error("JOIN EVENT ERROR:", e);
return res.status(500).json({ success: false, msg: "Internal error" });
}
}

/**
* POST /api/events/:eventId/join/guest
* Join an event as a guest (no registered user)
*
* @param req.params.eventId - Event ID to join (required)
* @param req.body.name - Display name of the guest participant (required)
*
* @returns 200 with participant info on success
* @returns 400 if required fields are missing or event is full
* @returns 404 if event is not found
*/
export async function joinEventAsGuest(req: Request, res: Response) {
try {
const { name } = req.body;
const { eventId } = req.params;

if (!name || !eventId) {
return res.status(400).json({
success: false,
msg: "Missing fields: guest name and eventId are required",
});
}

const event = await Event.findById(eventId);
if (!event) {
return res.status(404).json({ success: false, msg: "Event not found" });
}

if (event.participantIds.length >= event.maxParticipant) {
return res.status(400).json({ success: false, msg: "Event is full" });
}

// Create guest participant (userId is null)
const participant = await Participant.create({
userId: null,
name,
eventId,
});

const participantId = participant._id as Types.ObjectId;

// Add participant to event
await Event.updateOne(
{ _id: eventId },
{ $addToSet: { participantIds: participantId } }
);

// Emit socket
console.log("Room socket:", eventId);
console.log("Participant data:", { participantId, name });

if (!req.io) {
console.error("ERROR: req.io is undefined!");
} else {
const room = req.io.to(eventId);
console.log("Room object:", room);

room.emit("participant-joined", {
participantId,
name,
});

console.log("Socket event emitted successfully");
}

return res.json({
success: true,
participant,
});
} catch (e: any) {
if (e.code === 11000) {
return res.status(409).json({
success: false,
msg: "This name is already taken in this event",
});
}
console.error("JOIN GUEST ERROR:", e);
return res.status(500).json({ success: false, msg: "Internal error" });
}
}
Loading