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/controllers/user_controller.ts b/shatter-backend/src/controllers/user_controller.ts index 877b53a..ea0732e 100644 --- a/shatter-backend/src/controllers/user_controller.ts +++ b/shatter-backend/src/controllers/user_controller.ts @@ -1,41 +1,50 @@ -import { Request, Response } from "express"; -import { User } from "../models/user_model"; +// import req and res types for type safety +import { Request, Response } from 'express'; +import { User } from '../models/user_model.ts'; // imports user model created with mongoose // controller: GET /api/users // This function handles GET reqs to /api/users // It fetches all users from MongoDB and sends them as json export const getUsers = async (_req: Request, res: Response) => { - try { - const users = await User.find().lean(); - res.json(users); - } catch (err: any) { - console.error("GET /api/users error:", err); - res.status(500).json({ - error: "Failed to fetch users", - message: err.message, - }); - } + try { + // retrieves all docs from "users" collection + const users = await User.find().lean(); // .lean() returns plain JS objects instead of Mongoose docs, may change incase we need extra model methods later + res.json(users); // sends list of users back as JSON response + } catch (err) { // log the error if something goes wrong + console.error('GET /api/users error:', err); + res.status(500).json({ error: 'Failed to fetch users' }); + } }; + // controller: POST /api/users // reads data from req body, vailidates it and creates a new user export const createUser = async (req: Request, res: Response) => { - try { - const { name, email } = req.body ?? {}; + try { + // Destructure the req body sent by the client + // The ?? {} ensures we don't get error if req.body is undefined + const { name, email, password} = req.body ?? {}; - if (!name || !email) { - return res.status(400).json({ error: "name and email required" }); - } + // Basic validation to ensure both name and email are provided + // if not respond with bad request and stop further processes + if (!name || !email || !password) { + return res.status(400).json({ error: 'name, email required, and password' }); + } - const user = await User.create({ name, email }); - res.status(201).json(user); - } catch (err: any) { - if (err?.code === 11000) { - return res.status(409).json({ error: "email already exists" }); + // create a new user doc in DB using Mongoose's .create() + const user = await User.create({ name, email, password}); + // respond with "created" and send back created user as JSON + res.status(201).json(user); + } catch(err: any) { + // Handle duplicate email error + // Mongo DB rejects duplicat value since we have email marked as 'unique' + if (err?.code === 11000) { + return res.status(409).json({ error: 'email already exists' }); + } + + // for all other errors, log them and return generic 500 response + console.error('POST /api/users error:', err); + res.status(500).json({error: 'Failed to create user' }); } - - console.error("POST /api/users error:", err); - res.status(500).json({ error: "Failed to create user" }); - } }; 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/models/user_model.ts b/shatter-backend/src/models/user_model.ts index 23c1633..3fb1880 100644 --- a/shatter-backend/src/models/user_model.ts +++ b/shatter-backend/src/models/user_model.ts @@ -9,11 +9,7 @@ import { Schema, model } from 'mongoose'; export interface IUser { name: string; email: string; - passwordHash: string; - lastLogin?: Date; - passwordChangedAt?: Date; - createdAt?: Date; - updatedAt?: Date; + password: string; } // Create the Mongoose Schema (the database blueprint) @@ -31,26 +27,13 @@ const UserSchema = new Schema( type: String, required: true, trim: true, - lowercase: true, - unique: true, - index: true, - match: [ - /^[^\s@]+@[^\s@]+\.[^\s@]{2,}$/, - 'Please provide a valid email address' - ] + lowercase: true, // converts all emails to lowercase before saving for consistency + unique: true // enforce uniqueness, error 11000 if duplicate is detected }, - passwordHash: { + password: { type: String, required: true, - select: false // Don't return in queries by default - }, - lastLogin: { - type: Date, - default: null - }, - passwordChangedAt: { - type: Date, - default: null + select: false // exclude password field by default when querying users for security } }, { @@ -61,13 +44,6 @@ const UserSchema = new Schema( } ); -// Add middleware to auto-update passwordChangedAt -UserSchema.pre('save', function (next) { - if (this.isModified('passwordHash') && !this.isNew) { - this.passwordChangedAt = new Date(); - } - next(); -}); // create and export mongoose model // model is simply a wrapper around schema that gives access to MongoDB opeprations 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/routes/user_route.ts b/shatter-backend/src/routes/user_route.ts index a86e2cf..c26b3e8 100644 --- a/shatter-backend/src/routes/user_route.ts +++ b/shatter-backend/src/routes/user_route.ts @@ -1,9 +1,16 @@ +// router is like a mini-express app that allows grouping of related routes together import { Router } from 'express'; -import { getUsers, createUser } from '../controllers/user_controller'; +import { getUsers, createUser } from '../controllers/user_controller.ts'; +// Importing controller functions that handle logic for each route +// These function define what happens when a req is received +// creating new router instance const router = Router(); -router.get('/', getUsers); -router.post('/', createUser); +// Defining routes for the /api/users path +router.get('/', getUsers); // when GET req is made, run getUsers func +router.post('/', createUser); // when POST req is made, run creatUser func +// Export the router so it can be used in app.ts +// app.ts imports router and mounts it under '/api/users' export default router; 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(); +}