diff --git a/shatter-backend/package-lock.json b/shatter-backend/package-lock.json index b537f61..0c4405a 100644 --- a/shatter-backend/package-lock.json +++ b/shatter-backend/package-lock.json @@ -479,6 +479,7 @@ "integrity": "sha512-uWN8YqxXxqFMX2RqGOrumsKeti4LlmIMIyV0lgut4jx7KQBcBiW6vkDtIBvHnHIquwNfJhk8v2OtmO8zXWHfPA==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "undici-types": "~7.16.0" } @@ -605,6 +606,7 @@ "integrity": "sha512-BnOroVl1SgrPLywqxyqdJ4l3S2MsKVLDVxZvjI1Eoe8ev2r3kGDo+PcMihNmDE+6/KjkTubSJnmqGZZjQSBq/g==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "@typescript-eslint/scope-manager": "8.46.2", "@typescript-eslint/types": "8.46.2", @@ -836,6 +838,7 @@ "integrity": "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==", "dev": true, "license": "MIT", + "peer": true, "bin": { "acorn": "bin/acorn" }, @@ -1343,6 +1346,7 @@ "integrity": "sha512-t5aPOpmtJcZcz5UJyY2GbvpDlsK5E8JqRqoKtfiKE3cNh437KIqfJr3A3AKf5k64NPx6d0G3dno6XDY05PqPtw==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "@eslint-community/eslint-utils": "^4.8.0", "@eslint-community/regexpp": "^4.12.1", @@ -2053,6 +2057,7 @@ "integrity": "sha512-ekilCSN1jwRvIbgeg/57YFh8qQDNbwDb9xT/qu2DAHbFFZUicIl4ygVaAvzveMhMVr3LnpSKTNnwt8PoOfmKhQ==", "dev": true, "license": "MIT", + "peer": true, "bin": { "jiti": "lib/jiti-cli.mjs" } @@ -3234,6 +3239,7 @@ "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==", "dev": true, "license": "Apache-2.0", + "peer": true, "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" diff --git a/shatter-backend/src/app.ts b/shatter-backend/src/app.ts index 11e7050..70d0daa 100644 --- a/shatter-backend/src/app.ts +++ b/shatter-backend/src/app.ts @@ -1,6 +1,7 @@ import express from 'express'; 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'; const app = express(); @@ -12,5 +13,6 @@ app.get('/', (_req, res) => { app.use('/api/users', userRoutes); app.use('/api/auth', authRoutes); +app.use('/api/events', eventRoutes); export default app; diff --git a/shatter-backend/src/controllers/event_controller.ts b/shatter-backend/src/controllers/event_controller.ts new file mode 100644 index 0000000..4f06d9b --- /dev/null +++ b/shatter-backend/src/controllers/event_controller.ts @@ -0,0 +1,63 @@ +import { Request, Response } from "express"; +import { Event } from "../models/event_model"; +import "../models/participant_model"; + +import {generateEventId, generateJoinCode} from "../utils/event_utils"; + + +export async function createEvent(req: Request, res: Response) { + try { + const { name, description, startDate, endDate, maxParticipant, currentState, createdBy } = req.body; + + if (!createdBy) { + return res.status(400).json({ success: false, error: "createdBy email is required" }); + } + + const eventId = generateEventId(); + const joinCode = generateJoinCode(); + + const event = new Event({ + eventId, + name, + description, + joinCode, + startDate, + endDate, + maxParticipant, + participants: [], + currentState, + createdBy, // required email field + }); + + const savedEvent = await event.save(); + + res.status(201).json({ success: true, event: savedEvent }); + } catch (err: any) { + res.status(500).json({ success: false, error: err.message }); + } +} + + +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" }); + } + + // Find event by joinCode and populate participants + const event = await Event.findOne({ joinCode }).populate("participants"); + + 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 }); + } +} \ No newline at end of file diff --git a/shatter-backend/src/models/event_model.ts b/shatter-backend/src/models/event_model.ts new file mode 100644 index 0000000..fd5d74b --- /dev/null +++ b/shatter-backend/src/models/event_model.ts @@ -0,0 +1,56 @@ +import mongoose, { Schema, model, Document, Types } from "mongoose"; +import {User} from "../models/user_model"; + +import { IParticipant } from "./participant_model"; + +export interface IEvent extends Document { + eventId: string; + name: string; + description: string; + joinCode: string; + startDate: Date; + endDate: Date; + maxParticipant: number; + participants: mongoose.Types.DocumentArray; + currentState: string; + createdBy: string; +} + +const EventSchema = new Schema( + { + eventId: { type: String, required: true, unique: true }, + name: { type: String, required: true }, + description: { type: String, required: true }, + joinCode: { type: String, required: true, unique: true }, + startDate: { type: Date, required: true }, + endDate: { type: Date, required: true }, + maxParticipant: { type: Number, required: true }, + participants: [{ type: Types.ObjectId, ref: "Participant" }], + currentState: { type: String, required: true }, + createdBy: { + type: String, + required: true, + validate: { + validator: async function (email: string) { + const user = await User.findOne({ email }); + return !!user; // true if user exists + }, + message: "User with this email does not exist" + } + } + }, + { + timestamps: true, + } +); + +// Optional validation: ensure endDate is after startDate +EventSchema.pre("save", function (next) { + if (this.endDate <= this.startDate) { + next(new Error("endDate must be after startDate")); + } else { + next(); + } +}); + +export const Event = model("Event", EventSchema); diff --git a/shatter-backend/src/models/participant_model.ts b/shatter-backend/src/models/participant_model.ts new file mode 100644 index 0000000..8de7400 --- /dev/null +++ b/shatter-backend/src/models/participant_model.ts @@ -0,0 +1,26 @@ +import { Schema, model, Document } from "mongoose"; + +export interface IParticipant extends Document { + participantId: string | null; + name: string; + eventId: string; +} + +const ParticipantSchema = new Schema({ + participantId: { + type: String, + default: null, + }, + + name: { + type: String, + required: true, + }, + + eventId: { + type: String, + required: true, + }, +}); + +export const Participant = model("Participant", ParticipantSchema); diff --git a/shatter-backend/src/routes/event_routes.ts b/shatter-backend/src/routes/event_routes.ts new file mode 100644 index 0000000..fe95fb1 --- /dev/null +++ b/shatter-backend/src/routes/event_routes.ts @@ -0,0 +1,11 @@ +import { Router } from 'express'; +import { createEvent, getEventByJoinCode } from '../controllers/event_controller'; + +const router = Router(); + +// POST /api/events - create a new event +router.post('/createEvent', createEvent); +router.get("/event/:joinCode", getEventByJoinCode); + + +export default router; \ No newline at end of file diff --git a/shatter-backend/src/utils/event_utils.ts b/shatter-backend/src/utils/event_utils.ts new file mode 100644 index 0000000..58023d6 --- /dev/null +++ b/shatter-backend/src/utils/event_utils.ts @@ -0,0 +1,18 @@ +import crypto from "crypto"; + +/** + * Generates a random hash string for eventId + * Example: 3f5a9c7d2e8b1a6f4c9d0e2b7a1c8f3d + */ +export function generateEventId(): string { + return crypto.randomBytes(16).toString("hex"); +} + +/** + * Generates a random 8-digit number string for joinCode + * Example: "48392017" + */ +export function generateJoinCode(): string { + const code = Math.floor(10000000 + Math.random() * 90000000); + return code.toString(); +}