Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions shatter-backend/package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 2 additions & 0 deletions shatter-backend/src/app.ts
Original file line number Diff line number Diff line change
@@ -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();

Expand All @@ -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;
63 changes: 63 additions & 0 deletions shatter-backend/src/controllers/event_controller.ts
Original file line number Diff line number Diff line change
@@ -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 });
}
}
61 changes: 35 additions & 26 deletions shatter-backend/src/controllers/user_controller.ts
Original file line number Diff line number Diff line change
@@ -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" });
}
};
56 changes: 56 additions & 0 deletions shatter-backend/src/models/event_model.ts
Original file line number Diff line number Diff line change
@@ -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<IParticipant>;
currentState: string;
createdBy: string;
}

const EventSchema = new Schema<IEvent>(
{
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<IEvent>("Event", EventSchema);
26 changes: 26 additions & 0 deletions shatter-backend/src/models/participant_model.ts
Original file line number Diff line number Diff line change
@@ -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<IParticipant>({
participantId: {
type: String,
default: null,
},

name: {
type: String,
required: true,
},

eventId: {
type: String,
required: true,
},
});

export const Participant = model<IParticipant>("Participant", ParticipantSchema);
34 changes: 5 additions & 29 deletions shatter-backend/src/models/user_model.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand All @@ -31,26 +27,13 @@ const UserSchema = new Schema<IUser>(
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
}
},
{
Expand All @@ -61,13 +44,6 @@ const UserSchema = new Schema<IUser>(
}
);

// 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
Expand Down
11 changes: 11 additions & 0 deletions shatter-backend/src/routes/event_routes.ts
Original file line number Diff line number Diff line change
@@ -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;
13 changes: 10 additions & 3 deletions shatter-backend/src/routes/user_route.ts
Original file line number Diff line number Diff line change
@@ -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;
18 changes: 18 additions & 0 deletions shatter-backend/src/utils/event_utils.ts
Original file line number Diff line number Diff line change
@@ -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();
}
Loading