diff --git a/shatter-backend/package-lock.json b/shatter-backend/package-lock.json index 83686b5..764bed1 100644 --- a/shatter-backend/package-lock.json +++ b/shatter-backend/package-lock.json @@ -10,10 +10,12 @@ "license": "ISC", "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": { @@ -22,6 +24,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", @@ -366,6 +369,12 @@ "node": ">= 8" } }, + "node_modules/@socket.io/component-emitter": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/@socket.io/component-emitter/-/component-emitter-3.1.2.tgz", + "integrity": "sha512-9BCxFwvbGg/RsZK9tjXd8s4UcwR0MWeFQ1XEKIQVVvAGJyINdrqKMcTRyLoK8Rse1GjzLV9cwjWV1olXRWEXVA==", + "license": "MIT" + }, "node_modules/@tsconfig/node10": { "version": "1.0.11", "resolved": "https://registry.npmjs.org/@tsconfig/node10/-/node10-1.0.11.tgz", @@ -422,6 +431,15 @@ "@types/node": "*" } }, + "node_modules/@types/cors": { + "version": "2.8.19", + "resolved": "https://registry.npmjs.org/@types/cors/-/cors-2.8.19.tgz", + "integrity": "sha512-mFNylyeyqN93lfe/9CSxOGREz8cpzAhH+E93xJ4xWQf62V8sQ/24reV2nyzUWM6H6Xji+GGHpkbLe7pVoUEskg==", + "license": "MIT", + "dependencies": { + "@types/node": "*" + } + }, "node_modules/@types/estree": { "version": "1.0.8", "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.8.tgz", @@ -497,9 +515,7 @@ "version": "24.9.2", "resolved": "https://registry.npmjs.org/@types/node/-/node-24.9.2.tgz", "integrity": "sha512-uWN8YqxXxqFMX2RqGOrumsKeti4LlmIMIyV0lgut4jx7KQBcBiW6vkDtIBvHnHIquwNfJhk8v2OtmO8zXWHfPA==", - "dev": true, "license": "MIT", - "peer": true, "dependencies": { "undici-types": "~7.16.0" } @@ -551,6 +567,16 @@ "@types/node": "*" } }, + "node_modules/@types/socket.io": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/@types/socket.io/-/socket.io-3.0.1.tgz", + "integrity": "sha512-XSma2FhVD78ymvoxYV4xGXrIH/0EKQ93rR+YR0Y+Kw1xbPzLDCip/UWSejZ08FpxYeYNci/PZPQS9anrvJRqMA==", + "dev": true, + "license": "MIT", + "dependencies": { + "socket.io": "*" + } + }, "node_modules/@types/strip-bom": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/@types/strip-bom/-/strip-bom-3.0.0.tgz", @@ -626,7 +652,6 @@ "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", @@ -858,7 +883,6 @@ "integrity": "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==", "dev": true, "license": "MIT", - "peer": true, "bin": { "acorn": "bin/acorn" }, @@ -957,6 +981,15 @@ "dev": true, "license": "MIT" }, + "node_modules/base64id": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/base64id/-/base64id-2.0.0.tgz", + "integrity": "sha512-lGe34o6EHj9y3Kts9R4ZYs/Gr+6N7MCaMlIFA3F1R2O5/m7K06AxfSeO5530PEERE6/WyEg3lsuyw4GHlPZHog==", + "license": "MIT", + "engines": { + "node": "^4.5.0 || >= 5.9" + } + }, "node_modules/bcryptjs": { "version": "3.0.3", "resolved": "https://registry.npmjs.org/bcryptjs/-/bcryptjs-3.0.3.tgz", @@ -1201,6 +1234,19 @@ "node": ">=6.6.0" } }, + "node_modules/cors": { + "version": "2.8.5", + "resolved": "https://registry.npmjs.org/cors/-/cors-2.8.5.tgz", + "integrity": "sha512-KIHbLJqu73RGr/hnbrO9uBeixNGuvSQjul/jdFvS/KFSIH1hWVd1ng7zOHx+YrEfInLG7q4n6GHQ9cDtxv/P6g==", + "license": "MIT", + "dependencies": { + "object-assign": "^4", + "vary": "^1" + }, + "engines": { + "node": ">= 0.10" + } + }, "node_modules/create-require": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/create-require/-/create-require-1.1.1.tgz", @@ -1326,6 +1372,95 @@ "node": ">= 0.8" } }, + "node_modules/engine.io": { + "version": "6.6.4", + "resolved": "https://registry.npmjs.org/engine.io/-/engine.io-6.6.4.tgz", + "integrity": "sha512-ZCkIjSYNDyGn0R6ewHDtXgns/Zre/NT6Agvq1/WobF7JXgFff4SeDroKiCO3fNJreU9YG429Sc81o4w5ok/W5g==", + "license": "MIT", + "dependencies": { + "@types/cors": "^2.8.12", + "@types/node": ">=10.0.0", + "accepts": "~1.3.4", + "base64id": "2.0.0", + "cookie": "~0.7.2", + "cors": "~2.8.5", + "debug": "~4.3.1", + "engine.io-parser": "~5.2.1", + "ws": "~8.17.1" + }, + "engines": { + "node": ">=10.2.0" + } + }, + "node_modules/engine.io-parser": { + "version": "5.2.3", + "resolved": "https://registry.npmjs.org/engine.io-parser/-/engine.io-parser-5.2.3.tgz", + "integrity": "sha512-HqD3yTBfnBxIrbnM1DoD6Pcq8NECnh8d4As1Qgh0z5Gg3jRRIqijury0CL3ghu/edArpUYiYqQiDUQBIs4np3Q==", + "license": "MIT", + "engines": { + "node": ">=10.0.0" + } + }, + "node_modules/engine.io/node_modules/accepts": { + "version": "1.3.8", + "resolved": "https://registry.npmjs.org/accepts/-/accepts-1.3.8.tgz", + "integrity": "sha512-PYAthTa2m2VKxuvSD3DPC/Gy+U+sOA1LAuT8mkmRuvw+NACSaeXEQ+NHcVF7rONl6qcaxV3Uuemwawk+7+SJLw==", + "license": "MIT", + "dependencies": { + "mime-types": "~2.1.34", + "negotiator": "0.6.3" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/engine.io/node_modules/debug": { + "version": "4.3.7", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.7.tgz", + "integrity": "sha512-Er2nc/H7RrMXZBFCEim6TCmMk02Z8vLC2Rbi1KEBggpo0fS6l0S1nnapwmIi3yW/+GOJap1Krg4w0Hg80oCqgQ==", + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/engine.io/node_modules/mime-db": { + "version": "1.52.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", + "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/engine.io/node_modules/mime-types": { + "version": "2.1.35", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz", + "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==", + "license": "MIT", + "dependencies": { + "mime-db": "1.52.0" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/engine.io/node_modules/negotiator": { + "version": "0.6.3", + "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-0.6.3.tgz", + "integrity": "sha512-+EUsqGPLsM+j/zdChZjsnX51g4XrHFOIXwfnCVPGlQk/k5giakcKsuxCObBRu6DSm9opw/O6slWbJdghQM4bBg==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, "node_modules/es-define-property": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz", @@ -1381,7 +1516,6 @@ "integrity": "sha512-t5aPOpmtJcZcz5UJyY2GbvpDlsK5E8JqRqoKtfiKE3cNh437KIqfJr3A3AKf5k64NPx6d0G3dno6XDY05PqPtw==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "@eslint-community/eslint-utils": "^4.8.0", "@eslint-community/regexpp": "^4.12.1", @@ -2092,7 +2226,6 @@ "integrity": "sha512-ekilCSN1jwRvIbgeg/57YFh8qQDNbwDb9xT/qu2DAHbFFZUicIl4ygVaAvzveMhMVr3LnpSKTNnwt8PoOfmKhQ==", "dev": true, "license": "MIT", - "peer": true, "bin": { "jiti": "lib/jiti-cli.mjs" } @@ -2527,6 +2660,15 @@ "node": ">=0.10.0" } }, + "node_modules/object-assign": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", + "integrity": "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/object-inspect": { "version": "1.13.4", "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.13.4.tgz", @@ -3091,6 +3233,141 @@ "integrity": "sha512-Rtlj66/b0ICeFzYTuNvX/EF1igRbbnGSvEyT79McoZa/DeGhMyC5pWKOEsZKnpkqtSeovd5FL/bjHWC3CIIvCQ==", "license": "MIT" }, + "node_modules/socket.io": { + "version": "4.8.1", + "resolved": "https://registry.npmjs.org/socket.io/-/socket.io-4.8.1.tgz", + "integrity": "sha512-oZ7iUCxph8WYRHHcjBEc9unw3adt5CmSNlppj/5Q4k2RIrhl8Z5yY2Xr4j9zj0+wzVZ0bxmYoGSzKJnRl6A4yg==", + "license": "MIT", + "dependencies": { + "accepts": "~1.3.4", + "base64id": "~2.0.0", + "cors": "~2.8.5", + "debug": "~4.3.2", + "engine.io": "~6.6.0", + "socket.io-adapter": "~2.5.2", + "socket.io-parser": "~4.2.4" + }, + "engines": { + "node": ">=10.2.0" + } + }, + "node_modules/socket.io-adapter": { + "version": "2.5.5", + "resolved": "https://registry.npmjs.org/socket.io-adapter/-/socket.io-adapter-2.5.5.tgz", + "integrity": "sha512-eLDQas5dzPgOWCk9GuuJC2lBqItuhKI4uxGgo9aIV7MYbk2h9Q6uULEh8WBzThoI7l+qU9Ast9fVUmkqPP9wYg==", + "license": "MIT", + "dependencies": { + "debug": "~4.3.4", + "ws": "~8.17.1" + } + }, + "node_modules/socket.io-adapter/node_modules/debug": { + "version": "4.3.7", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.7.tgz", + "integrity": "sha512-Er2nc/H7RrMXZBFCEim6TCmMk02Z8vLC2Rbi1KEBggpo0fS6l0S1nnapwmIi3yW/+GOJap1Krg4w0Hg80oCqgQ==", + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/socket.io-parser": { + "version": "4.2.4", + "resolved": "https://registry.npmjs.org/socket.io-parser/-/socket.io-parser-4.2.4.tgz", + "integrity": "sha512-/GbIKmo8ioc+NIWIhwdecY0ge+qVBSMdgxGygevmdHj24bsfgtCmcUUcQ5ZzcylGFHsN3k4HB4Cgkl96KVnuew==", + "license": "MIT", + "dependencies": { + "@socket.io/component-emitter": "~3.1.0", + "debug": "~4.3.1" + }, + "engines": { + "node": ">=10.0.0" + } + }, + "node_modules/socket.io-parser/node_modules/debug": { + "version": "4.3.7", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.7.tgz", + "integrity": "sha512-Er2nc/H7RrMXZBFCEim6TCmMk02Z8vLC2Rbi1KEBggpo0fS6l0S1nnapwmIi3yW/+GOJap1Krg4w0Hg80oCqgQ==", + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/socket.io/node_modules/accepts": { + "version": "1.3.8", + "resolved": "https://registry.npmjs.org/accepts/-/accepts-1.3.8.tgz", + "integrity": "sha512-PYAthTa2m2VKxuvSD3DPC/Gy+U+sOA1LAuT8mkmRuvw+NACSaeXEQ+NHcVF7rONl6qcaxV3Uuemwawk+7+SJLw==", + "license": "MIT", + "dependencies": { + "mime-types": "~2.1.34", + "negotiator": "0.6.3" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/socket.io/node_modules/debug": { + "version": "4.3.7", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.7.tgz", + "integrity": "sha512-Er2nc/H7RrMXZBFCEim6TCmMk02Z8vLC2Rbi1KEBggpo0fS6l0S1nnapwmIi3yW/+GOJap1Krg4w0Hg80oCqgQ==", + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/socket.io/node_modules/mime-db": { + "version": "1.52.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", + "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/socket.io/node_modules/mime-types": { + "version": "2.1.35", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz", + "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==", + "license": "MIT", + "dependencies": { + "mime-db": "1.52.0" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/socket.io/node_modules/negotiator": { + "version": "0.6.3", + "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-0.6.3.tgz", + "integrity": "sha512-+EUsqGPLsM+j/zdChZjsnX51g4XrHFOIXwfnCVPGlQk/k5giakcKsuxCObBRu6DSm9opw/O6slWbJdghQM4bBg==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, "node_modules/source-map": { "version": "0.6.1", "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", @@ -3358,7 +3635,6 @@ "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==", "dev": true, "license": "Apache-2.0", - "peer": true, "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" @@ -3395,7 +3671,6 @@ "version": "7.16.0", "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.16.0.tgz", "integrity": "sha512-Zz+aZWSj8LE6zoxD+xrjh4VfkIG8Ya6LvYkZqtUQGJPZjYl53ypCaUwWqo7eI0x66KBGeRo+mlBEkMSeSZ38Nw==", - "dev": true, "license": "MIT" }, "node_modules/unpipe": { @@ -3487,6 +3762,27 @@ "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==", "license": "ISC" }, + "node_modules/ws": { + "version": "8.17.1", + "resolved": "https://registry.npmjs.org/ws/-/ws-8.17.1.tgz", + "integrity": "sha512-6XQFvXTkbfUOZOKKILFG1PDK2NDQs4azKQl26T0YS5CxqWLgXajbPZ+h4gZekJyRqFU8pvnbAbbs/3TgRPy+GQ==", + "license": "MIT", + "engines": { + "node": ">=10.0.0" + }, + "peerDependencies": { + "bufferutil": "^4.0.1", + "utf-8-validate": ">=5.0.2" + }, + "peerDependenciesMeta": { + "bufferutil": { + "optional": true + }, + "utf-8-validate": { + "optional": true + } + } + }, "node_modules/xtend": { "version": "4.0.2", "resolved": "https://registry.npmjs.org/xtend/-/xtend-4.0.2.tgz", diff --git a/shatter-backend/package.json b/shatter-backend/package.json index 0eb8397..7f2e12d 100644 --- a/shatter-backend/package.json +++ b/shatter-backend/package.json @@ -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": { @@ -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", diff --git a/shatter-backend/src/app.ts b/shatter-backend/src/app.ts index 70d0daa..98fe109 100644 --- a/shatter-backend/src/app.ts +++ b/shatter-backend/src/app.ts @@ -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'; @@ -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'); }); diff --git a/shatter-backend/src/controllers/auth_controller.ts b/shatter-backend/src/controllers/auth_controller.ts index d796701..0c49a79 100644 --- a/shatter-backend/src/controllers/auth_controller.ts +++ b/shatter-backend/src/controllers/auth_controller.ts @@ -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 { @@ -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) { diff --git a/shatter-backend/src/controllers/event_controller.ts b/shatter-backend/src/controllers/event_controller.ts index 4f06d9b..ae04009 100644 --- a/shatter-backend/src/controllers/event_controller.ts +++ b/shatter-backend/src/controllers/event_controller.ts @@ -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(); @@ -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" }); @@ -60,4 +106,193 @@ export async function getEventByJoinCode(req: Request, res: Response) { } catch (err: any) { res.status(500).json({ success: false, error: err.message }); } -} \ No newline at end of file +} + +/** + * 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" }); + } +} diff --git a/shatter-backend/src/middleware/auth_middleware.ts b/shatter-backend/src/middleware/auth_middleware.ts index 69d4c1f..30c79b7 100644 --- a/shatter-backend/src/middleware/auth_middleware.ts +++ b/shatter-backend/src/middleware/auth_middleware.ts @@ -26,7 +26,8 @@ declare global { * Request must include: * Authorization: Bearer */ -export const authMiddleware = async ( + +export const authMiddleware = ( req: Request, res: Response, next: NextFunction @@ -50,7 +51,7 @@ export const authMiddleware = async ( }); } - if (parts[0] !== 'Bearer') { + if (parts[0].toLowerCase() !== 'bearer') { return res.status(401).json({ error: 'Invalid authorization format. Must start with "Bearer"', }); diff --git a/shatter-backend/src/models/event_model.ts b/shatter-backend/src/models/event_model.ts index fd5d74b..04dc55f 100644 --- a/shatter-backend/src/models/event_model.ts +++ b/shatter-backend/src/models/event_model.ts @@ -1,43 +1,34 @@ import mongoose, { Schema, model, Document, Types } from "mongoose"; -import {User} from "../models/user_model"; +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; + participantIds: Schema.Types.ObjectId[]; currentState: string; - createdBy: string; + createdBy: Schema.Types.ObjectId; } 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" }], + maxParticipant: { type: Number, required: true }, + participantIds: [{ type: Schema.Types.ObjectId, ref: "Participant" }], currentState: { type: String, required: true }, - createdBy: { - type: String, + createdBy: { + type: Schema.Types.ObjectId, 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, diff --git a/shatter-backend/src/models/participant_model.ts b/shatter-backend/src/models/participant_model.ts index 8de7400..f26a574 100644 --- a/shatter-backend/src/models/participant_model.ts +++ b/shatter-backend/src/models/participant_model.ts @@ -1,26 +1,26 @@ import { Schema, model, Document } from "mongoose"; export interface IParticipant extends Document { - participantId: string | null; + userId: Schema.Types.ObjectId | null; name: string; - eventId: string; + eventId: Schema.Types.ObjectId; } -const ParticipantSchema = new Schema({ - participantId: { - type: String, - default: null, - }, - - name: { - type: String, - required: true, - }, - - eventId: { - type: String, - required: true, - }, +const ParticipantSchema = new Schema({ + userId: { type: Schema.Types.ObjectId, ref: "User", default: null }, + name: { type: String, required: true }, + eventId: { type: Schema.Types.ObjectId, ref: "Event", required: true }, }); -export const Participant = model("Participant", ParticipantSchema); +ParticipantSchema.index( + { eventId: 1, name: 1 }, + { + unique: true, + collation: { locale: "en", strength: 2 }, + } +); + +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..a2d29e2 100644 --- a/shatter-backend/src/models/user_model.ts +++ b/shatter-backend/src/models/user_model.ts @@ -1,19 +1,20 @@ // Import Schema and model from the Mongoose library. // - Schema: defines the structure and rules for documents in a collection (like a blueprint). // - model: creates a model (class) that we use in code to read/write those documents. -import { Schema, model } from 'mongoose'; +import { Schema, model } from "mongoose"; // define TS interface for type safety // This helps IDE and compiler know what fields exist on a User export interface IUser { - name: string; - email: string; - passwordHash: string; - lastLogin?: Date; - passwordChangedAt?: Date; - createdAt?: Date; - updatedAt?: Date; + name: string; + email: string; + passwordHash: string; + lastLogin?: Date; + passwordChangedAt?: Date; + createdAt?: Date; + updatedAt?: Date; + eventHistoryIds: Schema.Types.ObjectId[]; } // Create the Mongoose Schema (the database blueprint) @@ -24,8 +25,8 @@ const UserSchema = new Schema( { name: { type: String, - required: true, // field is mandatory; Mongoose will throw error if missing - trim: true // removes extra space at start and end + required: true, // field is mandatory; Mongoose will throw error if missing + trim: true, // removes extra space at start and end }, email: { type: String, @@ -35,35 +36,41 @@ const UserSchema = new Schema( unique: true, index: true, match: [ - /^[^\s@]+@[^\s@]+\.[^\s@]{2,}$/, - 'Please provide a valid email address' - ] + /^[^\s@]+@[^\s@]+\.[^\s@]{2,}$/, + "Please provide a valid email address", + ], }, passwordHash: { type: String, required: true, - select: false // Don't return in queries by default + select: false, // Don't return in queries by default }, lastLogin: { type: Date, - default: null + default: null, }, passwordChangedAt: { type: Date, - default: null - } + default: null, + }, + eventHistoryIds: [ + { + type: Schema.Types.ObjectId, + ref: "Event", + }, + ], }, { // timestamps: true automatically adds two fields to each document: // - createdAt: Date when the document was first created // - updatedAt: Date when the document was last modified - timestamps: true + timestamps: true, } ); // Add middleware to auto-update passwordChangedAt -UserSchema.pre('save', function (next) { - if (this.isModified('passwordHash') && !this.isNew) { +UserSchema.pre("save", function (next) { + if (this.isModified("passwordHash") && !this.isNew) { this.passwordChangedAt = new Date(); } next(); @@ -74,4 +81,4 @@ UserSchema.pre('save', function (next) { // "User" is the model name // Mongoose will automatically use "users" as the collection name in MongoDB -export const User = model('User', UserSchema); +export const User = model("User", UserSchema); diff --git a/shatter-backend/src/routes/event_routes.ts b/shatter-backend/src/routes/event_routes.ts index fe95fb1..bc5fa58 100644 --- a/shatter-backend/src/routes/event_routes.ts +++ b/shatter-backend/src/routes/event_routes.ts @@ -1,11 +1,14 @@ import { Router } from 'express'; -import { createEvent, getEventByJoinCode } from '../controllers/event_controller'; +import { createEvent, getEventByJoinCode, joinEventAsUser, joinEventAsGuest } from '../controllers/event_controller'; +import { authMiddleware } from '../middleware/auth_middleware'; const router = Router(); -// POST /api/events - create a new event -router.post('/createEvent', createEvent); + +router.post("/createEvent", authMiddleware, createEvent); router.get("/event/:joinCode", getEventByJoinCode); +router.post("/:eventId/join/user", authMiddleware, joinEventAsUser); +router.post("/:eventId/join/guest", joinEventAsGuest); export default router; \ No newline at end of file diff --git a/shatter-backend/src/server.ts b/shatter-backend/src/server.ts index 257ba45..15a3808 100644 --- a/shatter-backend/src/server.ts +++ b/shatter-backend/src/server.ts @@ -1,27 +1,68 @@ -import 'dotenv/config'; -import mongoose from 'mongoose'; -import app from './app'; +import 'dotenv/config'; +import mongoose from 'mongoose'; +import http from 'http'; +import { Server as SocketIOServer } from 'socket.io'; +import app from './app'; // config -const PORT = process.env.PORT ? Number(process.env.PORT) : 4000; -const MONGODB_URI = process.env.MONGO_URI; +const PORT = process.env.PORT ? Number(process.env.PORT) : 4000; +const MONGODB_URI = process.env.MONGO_URI; async function start() { - try { - if (!MONGODB_URI) { - throw new Error('MONGODB_URI is not set'); - } - await mongoose.connect(MONGODB_URI); - console.log('Successfully connected to MongoDB'); - - // start listening for incoming HTTP requests on chosen port - app.listen(PORT, () => { - console.log(`Server running on http://localhost:${PORT}`); - }); - } catch (err) { - console.error('Failed to start server:', err); - process.exit(1); + try { + if (!MONGODB_URI) { + throw new Error("MONGODB_URI is not set"); } + await mongoose.connect(MONGODB_URI); + console.log("Successfully connected to MongoDB"); + + // // start listening for incoming HTTP requests on chosen port + // app.listen(PORT, () => { + // console.log(`Server running on http://localhost:${PORT}`); + // }); + + // Create HTTP server from Express app + const httpServer = http.createServer(app); + + // Setup Socket.IO + const io = new SocketIOServer(httpServer, { + cors: { + origin: "http://localhost:3000", // React frontend + methods: ["GET", "POST"], + credentials: true, + }, + transports: ["websocket", "polling"], // fallback to polling + }); + + app.set('socketio', io); + + // Socket.IO connection handler + io.on("connection", (socket) => { + console.log("Client connected:", socket.id); + + socket.on("join-event-room", (eventId: string) => { + socket.join(eventId); + console.log(`Socket ${socket.id} joined room ${eventId}`); + }); + + socket.on("leave-event-room", (eventId: string) => { + socket.leave(eventId); + console.log(`Socket ${socket.id} left room ${eventId}`); + }); + + socket.on("disconnect", () => { + console.log("Client disconnected:", socket.id); + }); + }); + + // Start server + httpServer.listen(PORT, () => { + console.log(`Server running on http://localhost:${PORT}`); + }); + } catch (err) { + console.error("Failed to start server:", err); + process.exit(1); + } } start(); diff --git a/shatter-backend/src/types/express/index.d.ts b/shatter-backend/src/types/express/index.d.ts new file mode 100644 index 0000000..9725032 --- /dev/null +++ b/shatter-backend/src/types/express/index.d.ts @@ -0,0 +1,9 @@ +import { Server as SocketIOServer } from "socket.io"; + +declare global { + namespace Express { + interface Request { + io: SocketIOServer; + } + } +} diff --git a/shatter-backend/src/utils/jwt_utils.ts b/shatter-backend/src/utils/jwt_utils.ts index 5442035..de3e2bc 100644 --- a/shatter-backend/src/utils/jwt_utils.ts +++ b/shatter-backend/src/utils/jwt_utils.ts @@ -1,8 +1,9 @@ import jwt from 'jsonwebtoken'; +import type { StringValue } from 'ms'; // Get JWT secret from .env const JWT_SECRET = process.env.JWT_SECRET || ''; -const JWT_EXPIRATION = '30d'; // set token to expire in 30 days +const JWT_EXPIRATION = '30d'; // Default token expiration (can be overridden by JWT_EXPIRATION env var) // Validate that secret actually exists if (!JWT_SECRET) { @@ -21,11 +22,14 @@ if (!JWT_SECRET) { */ export const generateToken = (userId: string): string => { try{ + // Get expiration from env or use default + const expiration = (process.env.JWT_EXPIRATION ?? JWT_EXPIRATION) as StringValue; + // create and sign the token const token = jwt.sign( { userId }, // payload - data we want to store JWT_SECRET, // Secret key - proves token is real - { expiresIn: JWT_EXPIRATION} // Options - token expires in 30 days + { expiresIn: expiration } // Options - token expires in 30 days ); return token; diff --git a/shatter-backend/tsconfig.json b/shatter-backend/tsconfig.json index bfaac61..c997406 100644 --- a/shatter-backend/tsconfig.json +++ b/shatter-backend/tsconfig.json @@ -8,7 +8,8 @@ "sourceMap": true, "outDir": "./dist", "rootDir": "./", - "lib": ["ES2021"] + "lib": ["ES2021"], + "typeRoots": ["./src/types", "./node_modules/@types"] }, "include": ["src/**/*", "api/**/*"], "exclude": ["node_modules", "dist"]