diff --git a/cohorts.json b/cohorts.json new file mode 100644 index 00000000..4199f632 --- /dev/null +++ b/cohorts.json @@ -0,0 +1,57 @@ +[ + { + "_id": { "$oid": "616c4b4c649eaa001dd50f81" }, + "inProgress": false, + "cohortSlug": "ft-wd-paris-2023-07-03", + "cohortName": "FT WD PARIS 2023 07", + "program": "Web Dev", + "campus": "Paris", + "startDate": "2023-07-03T00:00:00.000Z", + "endDate": "2023-09-08T00:00:00.000Z", + "programManager": "Sally Daher", + "leadTeacher": "Florian Aube", + "totalHours": 360 + }, + { + "_id": { "$oid": "616c4b4c649eaa001dd50f82" }, + "cohortSlug": "pt-ux-berlin-2023-02-06", + "cohortName": "PT UX BERLIN 2023 02", + "program": "UX/UI", + "format": "Part Time", + "campus": "Berlin", + "startDate": "2023-02-06T00:00:00.000Z", + "endDate": "2023-08-06T00:00:00.000Z", + "inProgress": false, + "programManager": "Alice Williams", + "leadTeacher": "Bob Johnson", + "totalHours": 360 + }, + { + "_id": { "$oid": "616c4b4c649eaa001dd50f83" }, + "cohortSlug": "ft-da-miami-2023-03-06", + "cohortName": "FT DA MIAMI 2023 03", + "program": "Data Analytics", + "format": "Full Time", + "campus": "Miami", + "startDate": "2023-03-06T00:00:00.000Z", + "endDate": "2023-05-15T00:00:00.000Z", + "inProgress": true, + "programManager": "Charlie Brown", + "leadTeacher": "Eva Edwards", + "totalHours": 360 + }, + { + "_id": { "$oid": "616c4b4c649eaa001dd50f84" }, + "cohortSlug": "pt-cy-paris-2023-04-03", + "cohortName": "PT CY PARIS 2023 04", + "program": "Cybersecurity", + "format": "Part Time", + "campus": "Paris", + "startDate": "2023-04-03T00:00:00.000Z", + "endDate": "2023-10-03T00:00:00.000Z", + "inProgress": false, + "programManager": "Eva Edwards", + "leadTeacher": "Frank Foster", + "totalHours": 360 + } +] diff --git a/server/.env.sample b/server/.env.sample deleted file mode 100644 index 0042a604..00000000 --- a/server/.env.sample +++ /dev/null @@ -1 +0,0 @@ -PORT=5005 \ No newline at end of file diff --git a/server/app.js b/server/app.js index dfe51f49..3cecbe82 100644 --- a/server/app.js +++ b/server/app.js @@ -1,36 +1,367 @@ +// server/app.js +require("dotenv").config(); + const express = require("express"); const morgan = require("morgan"); const cookieParser = require("cookie-parser"); -const PORT = 5005; +const path = require("path"); +const cors = require("cors"); +const mongoose = require("mongoose"); +const { isValidObjectId } = mongoose; -// STATIC DATA -// Devs Team - Import the provided files with JSON data of students and cohorts here: -// ... +// Auth deps +const bcrypt = require("bcryptjs"); +const jwtSign = require("jsonwebtoken"); +const { expressjwt: jwt } = require("express-jwt"); +// Models existentes +const Cohort = require("./models/Cohort.model"); +const Student = require("./models/Student.model"); -// INITIALIZE EXPRESS APP - https://expressjs.com/en/4x/api.html#express -const app = express(); +// ---- User Model (inline para simplificar o Day 5) ---- +const userSchema = new mongoose.Schema( + { + email: { + type: String, + required: true, + unique: true, + lowercase: true, + trim: true, + }, + password: { type: String, required: true }, // armazenamos o HASH aqui + name: { type: String, required: true, trim: true }, + }, + { timestamps: true } +); +const User = mongoose.models.User || mongoose.model("User", userSchema); +// ---- App / Middlewares ---- +const app = express(); +const PORT = process.env.PORT || 5005; -// MIDDLEWARE -// Research Team - Set up CORS middleware here: -// ... -app.use(express.json()); +app.use(cors({ origin: "http://localhost:5173" })); // ajuste se necessário app.use(morgan("dev")); -app.use(express.static("public")); +app.use(express.json()); app.use(express.urlencoded({ extended: false })); app.use(cookieParser()); +app.use(express.static(path.join(__dirname, "public"))); + +// Health & Docs +app.get("/", (_req, res) => res.json({ status: "ok", docs: "/docs" })); +app.get("/docs", (_req, res) => + res.sendFile(path.join(__dirname, "views", "docs.html")) +); + +// ---- JWT middleware (protege rotas) ---- +const requireAuth = jwt({ + secret: process.env.JWT_SECRET, + algorithms: ["HS256"], +}); + +// ===================== +// = AUTH ROUTES = +// ===================== + +// POST /auth/signup -> cria usuário (hash de senha) +app.post("/auth/signup", async (req, res, next) => { + try { + const { email, password, name } = req.body; + if (!email || !password || !name) { + return res + .status(400) + .json({ error: "email, password and name are required" }); + } + const existing = await User.findOne({ email }); + if (existing) + return res.status(409).json({ error: "Email already exists" }); + + const passwordHash = await bcrypt.hash(password, 10); + const user = await User.create({ email, password: passwordHash, name }); + + res.status(201).json({ _id: user._id, email: user.email, name: user.name }); + } catch (err) { + if (err?.name === "ValidationError") + return res.status(400).json({ error: err.message }); + if (err?.code === 11000) + return res.status(409).json({ error: "Email already exists" }); + next(err); + } +}); + +// POST /auth/login -> verifica credenciais e devolve JWT +app.post("/auth/login", async (req, res, next) => { + try { + const { email, password } = req.body; + if (!email || !password) + return res.status(400).json({ error: "email and password are required" }); + + const user = await User.findOne({ email }); + if (!user) return res.status(401).json({ error: "Invalid credentials" }); + + const ok = await bcrypt.compare(password, user.password); + if (!ok) return res.status(401).json({ error: "Invalid credentials" }); + + const token = jwtSign.sign( + { sub: user._id.toString(), email: user.email, name: user.name }, + process.env.JWT_SECRET, + { expiresIn: "7d" } + ); + + res.json({ + token, + user: { _id: user._id, email: user.email, name: user.name }, + }); + } catch (err) { + next(err); + } +}); + +// GET /auth/verify -> valida JWT (protegida) +app.get("/auth/verify", requireAuth, (req, res) => { + // express-jwt coloca o payload decodificado em req.auth + res.json({ ok: true, payload: req.auth }); +}); + +// ===================== +// = USER (secure) = +// ===================== + +// GET /api/users/:id -> protegido por JWT +app.get("/api/users/:id", requireAuth, async (req, res, next) => { + try { + const { id } = req.params; + if (!isValidObjectId(id)) + return res.status(400).json({ error: "Invalid id" }); + + const user = await User.findById(id).select("-password").lean(); + if (!user) return res.status(404).json({ error: "Not found" }); + res.json(user); + } catch (err) { + next(err); + } +}); +// ================================== +// = COHORT ROUTES (CRUD) = +// ================================== -// ROUTES - https://expressjs.com/en/starter/basic-routing.html -// Devs Team - Start working on the routes here: -// ... -app.get("/docs", (req, res) => { - res.sendFile(__dirname + "/views/docs.html"); +// CREATE +app.post("/api/cohorts", async (req, res, next) => { + try { + const created = await Cohort.create(req.body); + res.status(201).json(created); + } catch (err) { + if (err?.code === 11000) + return res.status(409).json({ error: "Cohort already exists" }); + if (err?.name === "ValidationError") + return res.status(400).json({ error: err.message }); + next(err); + } }); +// READ ALL +app.get("/api/cohorts", async (_req, res, next) => { + try { + const list = await Cohort.find().lean(); + res.json(list); + } catch (err) { + next(err); + } +}); + +// READ ONE +app.get("/api/cohorts/:cohortId", async (req, res, next) => { + try { + const { cohortId } = req.params; + if (!isValidObjectId(cohortId)) + return res.status(400).json({ error: "Invalid id" }); + const doc = await Cohort.findById(cohortId).lean(); + if (!doc) return res.status(404).json({ error: "Not found" }); + res.json(doc); + } catch (err) { + next(err); + } +}); + +// UPDATE +app.put("/api/cohorts/:cohortId", async (req, res, next) => { + try { + const { cohortId } = req.params; + if (!isValidObjectId(cohortId)) + return res.status(400).json({ error: "Invalid id" }); + const updated = await Cohort.findByIdAndUpdate(cohortId, req.body, { + new: true, + runValidators: true, + }); + if (!updated) return res.status(404).json({ error: "Not found" }); + res.json(updated); + } catch (err) { + if (err?.code === 11000) + return res.status(409).json({ error: "Cohort slug already exists" }); + if (err?.name === "ValidationError") + return res.status(400).json({ error: err.message }); + next(err); + } +}); + +// DELETE +app.delete("/api/cohorts/:cohortId", async (req, res, next) => { + try { + const { cohortId } = req.params; + if (!isValidObjectId(cohortId)) + return res.status(400).json({ error: "Invalid id" }); + const deleted = await Cohort.findByIdAndDelete(cohortId); + if (!deleted) return res.status(404).json({ error: "Not found" }); + res.status(204).send(); + } catch (err) { + next(err); + } +}); + +// =================================== +// = STUDENT ROUTES (CRUD) = +// =================================== + +// CREATE +app.post("/api/students", async (req, res, next) => { + try { + const body = req.body; // {firstName,lastName,email,phone?,cohort?} + if (body.cohort && !isValidObjectId(body.cohort)) { + return res.status(400).json({ error: "Invalid cohort id" }); + } + const created = await Student.create(body); + const populated = await Student.findById(created._id) + .populate("cohort", "cohortName cohortSlug program format") + .lean(); + res.status(201).json(populated); + } catch (err) { + if (err?.code === 11000) + return res.status(409).json({ error: "Email already exists" }); + if (err?.name === "ValidationError") + return res.status(400).json({ error: err.message }); + next(err); + } +}); + +// READ ALL (populate) +app.get("/api/students", async (_req, res, next) => { + try { + const list = await Student.find() + .populate("cohort", "cohortName cohortSlug program format") + .lean(); + res.json(list); + } catch (err) { + next(err); + } +}); + +// READ by cohort (populate) +app.get("/api/students/cohort/:cohortId", async (req, res, next) => { + try { + const { cohortId } = req.params; + if (!isValidObjectId(cohortId)) + return res.status(400).json({ error: "Invalid id" }); + const list = await Student.find({ cohort: cohortId }) + .populate("cohort", "cohortName cohortSlug program format") + .lean(); + res.json(list); + } catch (err) { + next(err); + } +}); + +// READ ONE (populate) +app.get("/api/students/:studentId", async (req, res, next) => { + try { + const { studentId } = req.params; + if (!isValidObjectId(studentId)) + return res.status(400).json({ error: "Invalid id" }); + const doc = await Student.findById(studentId) + .populate("cohort", "cohortName cohortSlug program format") + .lean(); + if (!doc) return res.status(404).json({ error: "Not found" }); + res.json(doc); + } catch (err) { + next(err); + } +}); + +// UPDATE (return populated) +app.put("/api/students/:studentId", async (req, res, next) => { + try { + const { studentId } = req.params; + if (!isValidObjectId(studentId)) + return res.status(400).json({ error: "Invalid id" }); + if (req.body.cohort && !isValidObjectId(req.body.cohort)) { + return res.status(400).json({ error: "Invalid cohort id" }); + } + await Student.findByIdAndUpdate(studentId, req.body, { + new: false, + runValidators: true, + }); + const updated = await Student.findById(studentId) + .populate("cohort", "cohortName cohortSlug program format") + .lean(); + if (!updated) return res.status(404).json({ error: "Not found" }); + res.json(updated); + } catch (err) { + if (err?.code === 11000) + return res.status(409).json({ error: "Email already exists" }); + if (err?.name === "ValidationError") + return res.status(400).json({ error: err.message }); + next(err); + } +}); + +// DELETE +app.delete("/api/students/:studentId", async (req, res, next) => { + try { + const { studentId } = req.params; + if (!isValidObjectId(studentId)) + return res.status(400).json({ error: "Invalid id" }); + const deleted = await Student.findByIdAndDelete(studentId); + if (!deleted) return res.status(404).json({ error: "Not found" }); + res.status(204).send(); + } catch (err) { + next(err); + } +}); + +// ======================== +// = ERROR MIDDLEWARES = +// ======================== +app.use((err, _req, res, _next) => { + // express-jwt errors + if (err?.name === "UnauthorizedError") { + return res.status(401).json({ error: "Invalid or missing token" }); + } + // mongoose/common + if (err?.name === "ValidationError") + return res.status(400).json({ error: err.message }); + if (err?.name === "CastError") + return res.status(400).json({ error: "Invalid id" }); + if (err?.code === 11000) + return res.status(409).json({ error: "Duplicate key" }); + + console.error(err); + res.status(500).json({ error: "Internal Server Error" }); +}); + +app.use((_req, res) => res.status(404).json({ error: "Not found" })); + +// ---- Mongo connection + start ---- +if (!process.env.MONGODB_URI) { + console.error("❌ Missing MONGODB_URI in .env"); + process.exit(1); +} -// START SERVER -app.listen(PORT, () => { - console.log(`Server listening on port ${PORT}`); -}); \ No newline at end of file +mongoose + .connect(process.env.MONGODB_URI) + .then(() => { + console.log("MongoDB connected ✅"); + app.listen(PORT, () => console.log(`Server listening on port ${PORT}`)); + }) + .catch((err) => { + console.error("MongoDB connection error ❌", err.message); + process.exit(1); + }); diff --git a/server/cohort-tools-project.code-workspace b/server/cohort-tools-project.code-workspace new file mode 100644 index 00000000..c8a17f26 --- /dev/null +++ b/server/cohort-tools-project.code-workspace @@ -0,0 +1,9 @@ +{ + "folders": [ + { + "name": "cohort-tools-project", + "path": ".." + } + ], + "settings": {} +} diff --git a/server/controllers/cohorts.controller.js b/server/controllers/cohorts.controller.js new file mode 100644 index 00000000..38ec3d09 --- /dev/null +++ b/server/controllers/cohorts.controller.js @@ -0,0 +1,55 @@ +const Cohort = require("../models/Cohort.model"); + +exports.index = async (_req, res, next) => { + try { + res.json(await Cohort.find().lean()); + } catch (e) { + next(e); + } +}; + +exports.show = async (req, res, next) => { + try { + const doc = await Cohort.findById(req.params.cohortId).lean(); + if (!doc) return res.status(404).json({ error: "Not found" }); + res.json(doc); + } catch (e) { + next(e); + } +}; + +exports.create = async (req, res, next) => { + try { + const created = await Cohort.create(req.body); + res.status(201).location(`/api/cohorts/${created._id}`).json(created); + } catch (e) { + next(e); + } +}; + +exports.update = async (req, res, next) => { + try { + const updated = await Cohort.findByIdAndUpdate( + req.params.cohortId, + req.body, + { + new: true, + runValidators: true, + } + ); + if (!updated) return res.status(404).json({ error: "Not found" }); + res.json(updated); + } catch (e) { + next(e); + } +}; + +exports.destroy = async (req, res, next) => { + try { + const deleted = await Cohort.findByIdAndDelete(req.params.cohortId); + if (!deleted) return res.status(404).json({ error: "Not found" }); + res.status(204).send(); + } catch (e) { + next(e); + } +}; diff --git a/server/controllers/students.controller.js b/server/controllers/students.controller.js new file mode 100644 index 00000000..f6811148 --- /dev/null +++ b/server/controllers/students.controller.js @@ -0,0 +1,71 @@ +const Student = require("../models/Student.model"); +const populateSel = "cohortName cohortSlug program format"; + +exports.index = async (_req, res, next) => { + try { + const rows = await Student.find().populate("cohort", populateSel).lean(); + res.json(rows); + } catch (e) { + next(e); + } +}; + +exports.listByCohort = async (req, res, next) => { + try { + const rows = await Student.find({ cohort: req.params.cohortId }) + .populate("cohort", populateSel) + .lean(); + res.json(rows); + } catch (e) { + next(e); + } +}; + +exports.show = async (req, res, next) => { + try { + const doc = await Student.findById(req.params.studentId) + .populate("cohort", populateSel) + .lean(); + if (!doc) return res.status(404).json({ error: "Not found" }); + res.json(doc); + } catch (e) { + next(e); + } +}; + +exports.create = async (req, res, next) => { + try { + const created = await Student.create(req.body); + const populated = await Student.findById(created._id) + .populate("cohort", populateSel) + .lean(); + res.status(201).location(`/api/students/${created._id}`).json(populated); + } catch (e) { + next(e); + } +}; + +exports.update = async (req, res, next) => { + try { + await Student.findByIdAndUpdate(req.params.studentId, req.body, { + runValidators: true, + }); + const updated = await Student.findById(req.params.studentId) + .populate("cohort", populateSel) + .lean(); + if (!updated) return res.status(404).json({ error: "Not found" }); + res.json(updated); + } catch (e) { + next(e); + } +}; + +exports.destroy = async (req, res, next) => { + try { + const deleted = await Student.findByIdAndDelete(req.params.studentId); + if (!deleted) return res.status(404).json({ error: "Not found" }); + res.status(204).send(); + } catch (e) { + next(e); + } +}; diff --git a/server/middlewares/error.js b/server/middlewares/error.js new file mode 100644 index 00000000..614252b3 --- /dev/null +++ b/server/middlewares/error.js @@ -0,0 +1,12 @@ +exports.notFound = (_req, res) => res.status(404).json({ error: "Not found" }); + +exports.errorHandler = (err, _req, res, _next) => { + if (err?.name === "ValidationError") + return res.status(400).json({ error: err.message }); + if (err?.name === "CastError") + return res.status(400).json({ error: "Invalid id" }); + if (err?.code === 11000) + return res.status(409).json({ error: "Duplicate key" }); + console.error(err); + res.status(500).json({ error: "Internal Server Error" }); +}; diff --git a/server/models/Cohort.model.js b/server/models/Cohort.model.js new file mode 100644 index 00000000..46f1190e --- /dev/null +++ b/server/models/Cohort.model.js @@ -0,0 +1,18 @@ +const { Schema, model } = require("mongoose"); + +const cohortSchema = new Schema( + { + cohortSlug: { type: String, required: true, unique: true }, + cohortName: { type: String, required: true }, + program: { + type: String, + enum: ["Web Dev", "UX/UI", "Data Analytics", "Cybersecurity"], + required: true, + }, + format: { type: String, enum: ["Full Time", "Part Time"], required: true }, + inProgress: { type: Boolean, default: false }, + }, + { timestamps: true } +); + +module.exports = model("Cohort", cohortSchema); diff --git a/server/models/Student.model.js b/server/models/Student.model.js new file mode 100644 index 00000000..9a4f6633 --- /dev/null +++ b/server/models/Student.model.js @@ -0,0 +1,14 @@ +const { Schema, model } = require("mongoose"); + +const studentSchema = new Schema( + { + firstName: { type: String, required: true, trim: true }, + lastName: { type: String, required: true, trim: true }, + email: { type: String, required: true, unique: true, lowercase: true }, + phone: { type: String }, + cohort: { type: Schema.Types.ObjectId, ref: "Cohort" }, // se existir no JSON + }, + { timestamps: true } +); + +module.exports = model("Student", studentSchema); diff --git a/server/package-lock.json b/server/package-lock.json index 23e2f78a..d9657dde 100644 --- a/server/package-lock.json +++ b/server/package-lock.json @@ -9,12 +9,67 @@ "version": "1.0.0", "license": "ISC", "dependencies": { + "bcryptjs": "^3.0.2", "cookie-parser": "^1.4.6", + "cors": "^2.8.5", + "dotenv": "^17.2.1", "express": "^4.18.2", + "express-jwt": "^8.5.1", + "jsonwebtoken": "^9.0.2", + "mongoose": "^8.18.0", "morgan": "^1.10.0", "nodemon": "^3.0.1" } }, + "node_modules/@mongodb-js/saslprep": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/@mongodb-js/saslprep/-/saslprep-1.3.0.tgz", + "integrity": "sha512-zlayKCsIjYb7/IdfqxorK5+xUMyi4vOKcFy10wKJYc63NSdKI8mNME+uJqfatkPmOSMMUiojrL58IePKBm3gvQ==", + "license": "MIT", + "dependencies": { + "sparse-bitfield": "^3.0.3" + } + }, + "node_modules/@types/jsonwebtoken": { + "version": "9.0.10", + "resolved": "https://registry.npmjs.org/@types/jsonwebtoken/-/jsonwebtoken-9.0.10.tgz", + "integrity": "sha512-asx5hIG9Qmf/1oStypjanR7iKTv0gXQ1Ov/jfrX6kS/EO0OFni8orbmGCn0672NHR3kXHwpAwR+B368ZGN/2rA==", + "license": "MIT", + "dependencies": { + "@types/ms": "*", + "@types/node": "*" + } + }, + "node_modules/@types/ms": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/@types/ms/-/ms-2.1.0.tgz", + "integrity": "sha512-GsCCIZDE/p3i96vtEqx+7dBUGXrc7zeSK3wwPHIaRThS+9OhWIXRqzs4d6k1SVU8g91DrNRWxWUGhp5KXQb2VA==", + "license": "MIT" + }, + "node_modules/@types/node": { + "version": "24.3.0", + "resolved": "https://registry.npmjs.org/@types/node/-/node-24.3.0.tgz", + "integrity": "sha512-aPTXCrfwnDLj4VvXrm+UUCQjNEvJgNA8s5F1cvwQU+3KNltTOkBm1j30uNLyqqPNe7gE3KFzImYoZEfLhp4Yow==", + "license": "MIT", + "dependencies": { + "undici-types": "~7.10.0" + } + }, + "node_modules/@types/webidl-conversions": { + "version": "7.0.3", + "resolved": "https://registry.npmjs.org/@types/webidl-conversions/-/webidl-conversions-7.0.3.tgz", + "integrity": "sha512-CiJJvcRtIgzadHCYXw7dqEnMNRjhGZlYK05Mj9OyktqV8uVT8fD2BFOB7S1uwBE3Kj2Z+4UyPmFw/Ixgw/LAlA==", + "license": "MIT" + }, + "node_modules/@types/whatwg-url": { + "version": "11.0.5", + "resolved": "https://registry.npmjs.org/@types/whatwg-url/-/whatwg-url-11.0.5.tgz", + "integrity": "sha512-coYR071JRaHa+xoEvvYqvnIHaVqaYrLPbsufM9BF63HkwI5Lgmy2QR8Q5K/lYDYo5AK82wOvSOS0UsLTpTG7uQ==", + "license": "MIT", + "dependencies": { + "@types/webidl-conversions": "*" + } + }, "node_modules/abbrev": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/abbrev/-/abbrev-1.1.1.tgz", @@ -70,6 +125,15 @@ "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==" }, + "node_modules/bcryptjs": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/bcryptjs/-/bcryptjs-3.0.2.tgz", + "integrity": "sha512-k38b3XOZKv60C4E2hVsXTolJWfkGRMbILBIe2IBITXciy5bOsTKot5kDrf3ZfufQtQOUN5mXceUEpU1rTl9Uog==", + "license": "BSD-3-Clause", + "bin": { + "bcrypt": "bin/bcrypt" + } + }, "node_modules/binary-extensions": { "version": "2.2.0", "resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.2.0.tgz", @@ -121,6 +185,21 @@ "node": ">=8" } }, + "node_modules/bson": { + "version": "6.10.4", + "resolved": "https://registry.npmjs.org/bson/-/bson-6.10.4.tgz", + "integrity": "sha512-WIsKqkSC0ABoBJuT1LEX+2HEvNmNKKgnTAyd0fL8qzK4SH2i9NXg+t08YtdZp/V9IZ33cxe3iV4yM0qg8lMQng==", + "license": "Apache-2.0", + "engines": { + "node": ">=16.20.1" + } + }, + "node_modules/buffer-equal-constant-time": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/buffer-equal-constant-time/-/buffer-equal-constant-time-1.0.1.tgz", + "integrity": "sha512-zRpUiDwd/xk6ADqPMATG8vc9VPrkck7T07OIx0gnjmJAnHnTVXNQG3vfvWNuiZIkwu9KrKdA1iJKfsfTVxE6NA==", + "license": "BSD-3-Clause" + }, "node_modules/bytes": { "version": "3.1.2", "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.2.tgz", @@ -216,6 +295,19 @@ "resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.0.6.tgz", "integrity": "sha512-QADzlaHc8icV8I7vbaJXJwod9HWYp8uCqf1xa4OfNu1T7JVxQIrUgOWtHdNDtPiywmFbiS12VjotIXLrKM3orQ==" }, + "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/debug": { "version": "2.6.9", "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", @@ -241,6 +333,27 @@ "npm": "1.2.8000 || >= 1.4.16" } }, + "node_modules/dotenv": { + "version": "17.2.1", + "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-17.2.1.tgz", + "integrity": "sha512-kQhDYKZecqnM0fCnzI5eIv5L4cAe/iRI+HqMbO/hbRdTAeXDG+M9FjipUxNfbARuEg4iHIbhnhs78BCHNbSxEQ==", + "license": "BSD-2-Clause", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://dotenvx.com" + } + }, + "node_modules/ecdsa-sig-formatter": { + "version": "1.0.11", + "resolved": "https://registry.npmjs.org/ecdsa-sig-formatter/-/ecdsa-sig-formatter-1.0.11.tgz", + "integrity": "sha512-nagl3RYrbNv6kQkeJIpt6NJZy8twLB/2vtz6yN9Z4vRKHN4/QZJIEbqohALSgwKdnksuY3k5Addp5lg8sVoVcQ==", + "license": "Apache-2.0", + "dependencies": { + "safe-buffer": "^5.0.1" + } + }, "node_modules/ee-first": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz", @@ -308,6 +421,26 @@ "node": ">= 0.10.0" } }, + "node_modules/express-jwt": { + "version": "8.5.1", + "resolved": "https://registry.npmjs.org/express-jwt/-/express-jwt-8.5.1.tgz", + "integrity": "sha512-Dv6QjDLpR2jmdb8M6XQXiCcpEom7mK8TOqnr0/TngDKsG2DHVkO8+XnVxkJVN7BuS1I3OrGw6N8j5DaaGgkDRQ==", + "license": "MIT", + "dependencies": { + "@types/jsonwebtoken": "^9", + "express-unless": "^2.1.3", + "jsonwebtoken": "^9.0.0" + }, + "engines": { + "node": ">= 8.0.0" + } + }, + "node_modules/express-unless": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/express-unless/-/express-unless-2.1.3.tgz", + "integrity": "sha512-wj4tLMyCVYuIIKHGt0FhCtIViBcwzWejX0EjNxveAa6dG+0XBCQhMbx+PnkLkFCxLC69qoFrxds4pIyL88inaQ==", + "license": "MIT" + }, "node_modules/express/node_modules/cookie": { "version": "0.5.0", "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.5.0.tgz", @@ -526,6 +659,106 @@ "node": ">=0.12.0" } }, + "node_modules/jsonwebtoken": { + "version": "9.0.2", + "resolved": "https://registry.npmjs.org/jsonwebtoken/-/jsonwebtoken-9.0.2.tgz", + "integrity": "sha512-PRp66vJ865SSqOlgqS8hujT5U4AOgMfhrwYIuIhfKaoSCZcirrmASQr8CX7cUg+RMih+hgznrjp99o+W4pJLHQ==", + "license": "MIT", + "dependencies": { + "jws": "^3.2.2", + "lodash.includes": "^4.3.0", + "lodash.isboolean": "^3.0.3", + "lodash.isinteger": "^4.0.4", + "lodash.isnumber": "^3.0.3", + "lodash.isplainobject": "^4.0.6", + "lodash.isstring": "^4.0.1", + "lodash.once": "^4.0.0", + "ms": "^2.1.1", + "semver": "^7.5.4" + }, + "engines": { + "node": ">=12", + "npm": ">=6" + } + }, + "node_modules/jsonwebtoken/node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "license": "MIT" + }, + "node_modules/jwa": { + "version": "1.4.2", + "resolved": "https://registry.npmjs.org/jwa/-/jwa-1.4.2.tgz", + "integrity": "sha512-eeH5JO+21J78qMvTIDdBXidBd6nG2kZjg5Ohz/1fpa28Z4CcsWUzJ1ZZyFq/3z3N17aZy+ZuBoHljASbL1WfOw==", + "license": "MIT", + "dependencies": { + "buffer-equal-constant-time": "^1.0.1", + "ecdsa-sig-formatter": "1.0.11", + "safe-buffer": "^5.0.1" + } + }, + "node_modules/jws": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/jws/-/jws-3.2.2.tgz", + "integrity": "sha512-YHlZCB6lMTllWDtSPHz/ZXTsi8S00usEV6v1tjq8tOUZzw7DpSDWVXjXDre6ed1w/pd495ODpHZYSdkRTsa0HA==", + "license": "MIT", + "dependencies": { + "jwa": "^1.4.1", + "safe-buffer": "^5.0.1" + } + }, + "node_modules/kareem": { + "version": "2.6.3", + "resolved": "https://registry.npmjs.org/kareem/-/kareem-2.6.3.tgz", + "integrity": "sha512-C3iHfuGUXK2u8/ipq9LfjFfXFxAZMQJJq7vLS45r3D9Y2xQ/m4S8zaR4zMLFWh9AsNPXmcFfUDhTEO8UIC/V6Q==", + "license": "Apache-2.0", + "engines": { + "node": ">=12.0.0" + } + }, + "node_modules/lodash.includes": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/lodash.includes/-/lodash.includes-4.3.0.tgz", + "integrity": "sha512-W3Bx6mdkRTGtlJISOvVD/lbqjTlPPUDTMnlXZFnVwi9NKJ6tiAk6LVdlhZMm17VZisqhKcgzpO5Wz91PCt5b0w==", + "license": "MIT" + }, + "node_modules/lodash.isboolean": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/lodash.isboolean/-/lodash.isboolean-3.0.3.tgz", + "integrity": "sha512-Bz5mupy2SVbPHURB98VAcw+aHh4vRV5IPNhILUCsOzRmsTmSQ17jIuqopAentWoehktxGd9e/hbIXq980/1QJg==", + "license": "MIT" + }, + "node_modules/lodash.isinteger": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/lodash.isinteger/-/lodash.isinteger-4.0.4.tgz", + "integrity": "sha512-DBwtEWN2caHQ9/imiNeEA5ys1JoRtRfY3d7V9wkqtbycnAmTvRRmbHKDV4a0EYc678/dia0jrte4tjYwVBaZUA==", + "license": "MIT" + }, + "node_modules/lodash.isnumber": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/lodash.isnumber/-/lodash.isnumber-3.0.3.tgz", + "integrity": "sha512-QYqzpfwO3/CWf3XP+Z+tkQsfaLL/EnUlXWVkIk5FUPc4sBdTehEqZONuyRt2P67PXAk+NXmTBcc97zw9t1FQrw==", + "license": "MIT" + }, + "node_modules/lodash.isplainobject": { + "version": "4.0.6", + "resolved": "https://registry.npmjs.org/lodash.isplainobject/-/lodash.isplainobject-4.0.6.tgz", + "integrity": "sha512-oSXzaWypCMHkPC3NvBEaPHf0KsA5mvPrOPgQWDsbg8n7orZ290M0BmC/jgRZ4vcJ6DTAhjrsSYgdsW/F+MFOBA==", + "license": "MIT" + }, + "node_modules/lodash.isstring": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/lodash.isstring/-/lodash.isstring-4.0.1.tgz", + "integrity": "sha512-0wJxfxH1wgO3GrbuP+dTTk7op+6L41QCXbGINEmD+ny/G/eCqGzxyCsh7159S+mgDDcoarnBw6PC1PS5+wUGgw==", + "license": "MIT" + }, + "node_modules/lodash.once": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/lodash.once/-/lodash.once-4.1.1.tgz", + "integrity": "sha512-Sb487aTOCr9drQVL8pIxOzVhafOjZN9UU54hiN8PU3uAiSV7lx1yYNpbNmex2PK6dSJoNTSJUUswT651yww3Mg==", + "license": "MIT" + }, "node_modules/lru-cache": { "version": "6.0.0", "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz", @@ -545,6 +778,12 @@ "node": ">= 0.6" } }, + "node_modules/memory-pager": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/memory-pager/-/memory-pager-1.5.0.tgz", + "integrity": "sha512-ZS4Bp4r/Zoeq6+NLJpP+0Zzm0pR8whtGPf1XExKLJBAczGMnSi3It14OiNCStjQjM6NU1okjQGSxgEZN8eBYKg==", + "license": "MIT" + }, "node_modules/merge-descriptors": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-1.0.1.tgz", @@ -599,6 +838,90 @@ "node": "*" } }, + "node_modules/mongodb": { + "version": "6.18.0", + "resolved": "https://registry.npmjs.org/mongodb/-/mongodb-6.18.0.tgz", + "integrity": "sha512-fO5ttN9VC8P0F5fqtQmclAkgXZxbIkYRTUi1j8JO6IYwvamkhtYDilJr35jOPELR49zqCJgXZWwCtW7B+TM8vQ==", + "license": "Apache-2.0", + "dependencies": { + "@mongodb-js/saslprep": "^1.1.9", + "bson": "^6.10.4", + "mongodb-connection-string-url": "^3.0.0" + }, + "engines": { + "node": ">=16.20.1" + }, + "peerDependencies": { + "@aws-sdk/credential-providers": "^3.188.0", + "@mongodb-js/zstd": "^1.1.0 || ^2.0.0", + "gcp-metadata": "^5.2.0", + "kerberos": "^2.0.1", + "mongodb-client-encryption": ">=6.0.0 <7", + "snappy": "^7.2.2", + "socks": "^2.7.1" + }, + "peerDependenciesMeta": { + "@aws-sdk/credential-providers": { + "optional": true + }, + "@mongodb-js/zstd": { + "optional": true + }, + "gcp-metadata": { + "optional": true + }, + "kerberos": { + "optional": true + }, + "mongodb-client-encryption": { + "optional": true + }, + "snappy": { + "optional": true + }, + "socks": { + "optional": true + } + } + }, + "node_modules/mongodb-connection-string-url": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/mongodb-connection-string-url/-/mongodb-connection-string-url-3.0.2.tgz", + "integrity": "sha512-rMO7CGo/9BFwyZABcKAWL8UJwH/Kc2x0g72uhDWzG48URRax5TCIcJ7Rc3RZqffZzO/Gwff/jyKwCU9TN8gehA==", + "license": "Apache-2.0", + "dependencies": { + "@types/whatwg-url": "^11.0.2", + "whatwg-url": "^14.1.0 || ^13.0.0" + } + }, + "node_modules/mongoose": { + "version": "8.18.0", + "resolved": "https://registry.npmjs.org/mongoose/-/mongoose-8.18.0.tgz", + "integrity": "sha512-3TixPihQKBdyaYDeJqRjzgb86KbilEH07JmzV8SoSjgoskNTpa6oTBmDxeoF9p8YnWQoz7shnCyPkSV/48y3yw==", + "license": "MIT", + "dependencies": { + "bson": "^6.10.4", + "kareem": "2.6.3", + "mongodb": "~6.18.0", + "mpath": "0.9.0", + "mquery": "5.0.0", + "ms": "2.1.3", + "sift": "17.1.3" + }, + "engines": { + "node": ">=16.20.1" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/mongoose" + } + }, + "node_modules/mongoose/node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "license": "MIT" + }, "node_modules/morgan": { "version": "1.10.0", "resolved": "https://registry.npmjs.org/morgan/-/morgan-1.10.0.tgz", @@ -625,6 +948,50 @@ "node": ">= 0.8" } }, + "node_modules/mpath": { + "version": "0.9.0", + "resolved": "https://registry.npmjs.org/mpath/-/mpath-0.9.0.tgz", + "integrity": "sha512-ikJRQTk8hw5DEoFVxHG1Gn9T/xcjtdnOKIU1JTmGjZZlg9LST2mBLmcX3/ICIbgJydT2GOc15RnNy5mHmzfSew==", + "license": "MIT", + "engines": { + "node": ">=4.0.0" + } + }, + "node_modules/mquery": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/mquery/-/mquery-5.0.0.tgz", + "integrity": "sha512-iQMncpmEK8R8ncT8HJGsGc9Dsp8xcgYMVSbs5jgnm1lFHTZqMJTUWTDx1LBO8+mK3tPNZWFLBghQEIOULSTHZg==", + "license": "MIT", + "dependencies": { + "debug": "4.x" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/mquery/node_modules/debug": { + "version": "4.4.1", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.1.tgz", + "integrity": "sha512-KcKCqiftBJcZr++7ykoDIEwSa3XWowTfNPo92BYxjXiyYEVrUQh2aLyhxBCwww+heortUFxEJYcRzosstTEBYQ==", + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/mquery/node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "license": "MIT" + }, "node_modules/ms": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", @@ -700,6 +1067,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.12.3", "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.12.3.tgz", @@ -768,6 +1144,15 @@ "resolved": "https://registry.npmjs.org/pstree.remy/-/pstree.remy-1.1.8.tgz", "integrity": "sha512-77DZwxQmxKnu3aR542U+X8FypNzbfJ+C5XQDk3uWjWxn6151aIMGthWYRXTqT1E5oJvg+ljaa2OJi+VfvCOQ8w==" }, + "node_modules/punycode": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz", + "integrity": "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==", + "license": "MIT", + "engines": { + "node": ">=6" + } + }, "node_modules/qs": { "version": "6.11.0", "resolved": "https://registry.npmjs.org/qs/-/qs-6.11.0.tgz", @@ -913,6 +1298,12 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/sift": { + "version": "17.1.3", + "resolved": "https://registry.npmjs.org/sift/-/sift-17.1.3.tgz", + "integrity": "sha512-Rtlj66/b0ICeFzYTuNvX/EF1igRbbnGSvEyT79McoZa/DeGhMyC5pWKOEsZKnpkqtSeovd5FL/bjHWC3CIIvCQ==", + "license": "MIT" + }, "node_modules/simple-update-notifier": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/simple-update-notifier/-/simple-update-notifier-2.0.0.tgz", @@ -924,6 +1315,15 @@ "node": ">=10" } }, + "node_modules/sparse-bitfield": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/sparse-bitfield/-/sparse-bitfield-3.0.3.tgz", + "integrity": "sha512-kvzhi7vqKTfkh0PZU+2D2PIllw2ymqJKujUcyPMd9Y75Nv4nPbGJZXNhxsgdQab2BmlDct1YnfQCguEvHr7VsQ==", + "license": "MIT", + "dependencies": { + "memory-pager": "^1.0.2" + } + }, "node_modules/statuses": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.1.tgz", @@ -973,6 +1373,18 @@ "nodetouch": "bin/nodetouch.js" } }, + "node_modules/tr46": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/tr46/-/tr46-5.1.1.tgz", + "integrity": "sha512-hdF5ZgjTqgAntKkklYw0R03MG2x/bSzTtkxmIRw/sTNV8YXsCJ1tfLAX23lhxhHJlEf3CRCOCGGWw3vI3GaSPw==", + "license": "MIT", + "dependencies": { + "punycode": "^2.3.1" + }, + "engines": { + "node": ">=18" + } + }, "node_modules/type-is": { "version": "1.6.18", "resolved": "https://registry.npmjs.org/type-is/-/type-is-1.6.18.tgz", @@ -990,6 +1402,12 @@ "resolved": "https://registry.npmjs.org/undefsafe/-/undefsafe-2.0.5.tgz", "integrity": "sha512-WxONCrssBM8TSPRqN5EmsjVrsv4A8X12J4ArBiiayv3DyyG3ZlIg6yysuuSYdZsVz3TKcTg2fd//Ujd4CHV1iA==" }, + "node_modules/undici-types": { + "version": "7.10.0", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.10.0.tgz", + "integrity": "sha512-t5Fy/nfn+14LuOc2KNYg75vZqClpAiqscVvMygNnlsHBFpSXdJaYtXMcdNLpl/Qvc3P2cB3s6lOV51nqsFq4ag==", + "license": "MIT" + }, "node_modules/unpipe": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz", @@ -1014,6 +1432,28 @@ "node": ">= 0.8" } }, + "node_modules/webidl-conversions": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-7.0.0.tgz", + "integrity": "sha512-VwddBukDzu71offAQR975unBIGqfKZpM+8ZX6ySk8nYhVoo5CYaZyzt3YBvYtRtO+aoGlqxPg/B87NGVZ/fu6g==", + "license": "BSD-2-Clause", + "engines": { + "node": ">=12" + } + }, + "node_modules/whatwg-url": { + "version": "14.2.0", + "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-14.2.0.tgz", + "integrity": "sha512-De72GdQZzNTUBBChsXueQUnPKDkg/5A5zp7pFDuQAj5UFoENpiACU0wlCvzpAGnTkj++ihpKwKyYewn/XNUbKw==", + "license": "MIT", + "dependencies": { + "tr46": "^5.1.0", + "webidl-conversions": "^7.0.0" + }, + "engines": { + "node": ">=18" + } + }, "node_modules/yallist": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", diff --git a/server/package.json b/server/package.json index 7aa0847c..7d6e0427 100644 --- a/server/package.json +++ b/server/package.json @@ -5,7 +5,7 @@ "main": "app.js", "scripts": { "dev": "nodemon app.js", - "test": "echo \"Error: no test specified\" && exit 1" + "start": "node app.js" }, "repository": { "type": "git", @@ -18,8 +18,14 @@ }, "homepage": "https://github.com/ironhack-labs/cohort-tools-project#readme", "dependencies": { + "bcryptjs": "^3.0.2", "cookie-parser": "^1.4.6", + "cors": "^2.8.5", + "dotenv": "^17.2.1", "express": "^4.18.2", + "express-jwt": "^8.5.1", + "jsonwebtoken": "^9.0.2", + "mongoose": "^8.18.0", "morgan": "^1.10.0", "nodemon": "^3.0.1" } diff --git a/server/routes/cohorts.routes.js b/server/routes/cohorts.routes.js new file mode 100644 index 00000000..fa704182 --- /dev/null +++ b/server/routes/cohorts.routes.js @@ -0,0 +1,18 @@ +const router = require("express").Router(); +const mongoose = require("mongoose"); +const ctrl = require("../controllers/cohorts.controller"); + +router.get("/", ctrl.index); +router.get("/:cohortId", validate("cohortId"), ctrl.show); +router.post("/", ctrl.create); +router.put("/:cohortId", validate("cohortId"), ctrl.update); +router.delete("/:cohortId", validate("cohortId"), ctrl.destroy); + +function validate(param) { + return (req, res, next) => + mongoose.isValidObjectId(req.params[param]) + ? next() + : res.status(400).json({ error: "Invalid id" }); +} + +module.exports = router; diff --git a/server/routes/students.routes.js b/server/routes/students.routes.js new file mode 100644 index 00000000..0702b9b2 --- /dev/null +++ b/server/routes/students.routes.js @@ -0,0 +1,19 @@ +const router = require("express").Router(); +const mongoose = require("mongoose"); +const ctrl = require("../controllers/students.controller"); + +router.get("/", ctrl.index); +router.get("/cohort/:cohortId", validate("cohortId"), ctrl.listByCohort); +router.get("/:studentId", validate("studentId"), ctrl.show); +router.post("/", ctrl.create); +router.put("/:studentId", validate("studentId"), ctrl.update); +router.delete("/:studentId", validate("studentId"), ctrl.destroy); + +function validate(param) { + return (req, res, next) => + mongoose.isValidObjectId(req.params[param]) + ? next() + : res.status(400).json({ error: "Invalid id" }); +} + +module.exports = router; diff --git a/server/utils/express.js b/server/utils/express.js new file mode 100644 index 00000000..0f0b00d2 --- /dev/null +++ b/server/utils/express.js @@ -0,0 +1,12 @@ +const mongoose = require("mongoose"); + +exports.asyncHandler = (fn) => (req, res, next) => + Promise.resolve(fn(req, res, next)).catch(next); + +exports.validateObjectIdParam = (param) => (req, res, next) => { + const id = req.params[param]; + if (!mongoose.isValidObjectId(id)) { + return res.status(400).json({ error: "Invalid id" }); + } + next(); +}; diff --git a/students.json b/students.json new file mode 100644 index 00000000..1f0b4f65 --- /dev/null +++ b/students.json @@ -0,0 +1,86 @@ +[ + { + "_id": { "$oid": "616c4b4c649eaa001dd50f85" }, + "firstName": "Christine", + "lastName": "Clayton", + "email": "christine.clayton@example.com", + "phone": "567-890-1234", + "linkedinUrl": "https://linkedin.com/in/christineclaytonexample", + "languages": ["English", "Dutch"], + "program": "Web Dev", + "background": "Computer Engineering", + "image": "https://i.imgur.com/r8bo8u7.png", + "projects": [], + "cohort": { "$oid": "616c4b4c649eaa001dd50f81" } + }, + { + "_id": { "$oid": "616c4b4c649eaa001dd50f86" }, + "firstName": "Grace", + "lastName": "Green", + "email": "grace.green@example.com", + "phone": "901-234-5678", + "linkedinUrl": "https://linkedin.com/in/gracegreenexample", + "languages": ["English", "Portuguese"], + "program": "Web Dev", + "background": "Software Development", + "image": "https://i.imgur.com/r8bo8u7.png", + "projects": [], + "cohort": { "$oid": "616c4b4c649eaa001dd50f81" } + }, + { + "_id": { "$oid": "616c4b4c649eaa001dd50f87" }, + "firstName": "Jack", + "lastName": "Johnson", + "email": "jack.johnson@example.com", + "phone": "234-567-8901", + "linkedinUrl": "https://linkedin.com/in/jackjohnsonexample", + "languages": ["English", "French"], + "program": "Web Dev", + "background": "System Administration", + "image": "https://i.imgur.com/r8bo8u7.png", + "projects": [], + "cohort": { "$oid": "616c4b4c649eaa001dd50f81" } + }, + { + "_id": { "$oid": "616c4b4c649eaa001dd50f88" }, + "firstName": "John", + "lastName": "Doe", + "email": "john.doe@example.com", + "phone": "123-456-7890", + "linkedinUrl": "https://linkedin.com/in/johndoeexample", + "languages": ["English", "Spanish"], + "program": "UX/UI", + "background": "Computer Science", + "image": "https://i.imgur.com/r8bo8u7.png", + "projects": [], + "cohort": { "$oid": "616c4b4c649eaa001dd50f82" } + }, + { + "_id": { "$oid": "616c4b4c649eaa001dd50f89" }, + "firstName": "Katie", + "lastName": "King", + "email": "katie.king@example.com", + "phone": "345-678-9012", + "linkedinUrl": "https://linkedin.com/in/katiekingexample", + "languages": ["English", "German"], + "program": "UX/UI", + "background": "Information Systems", + "image": "https://i.imgur.com/r8bo8u7.png", + "projects": [], + "cohort": { "$oid": "616c4b4c649eaa001dd50f82" } + }, + { + "_id": { "$oid": "616c4b4c649eaa001dd50f8a" }, + "firstName": "Irene", + "lastName": "Iverson", + "email": "irene.iverson@example.com", + "phone": "123-456-7890", + "linkedinUrl": "https://linkedin.com/in/ireneiversonexample", + "languages": ["English", "Spanish"], + "program": "Data Analytics", + "background": "Economics", + "image": "https://i.imgur.com/r8bo8u7.png", + "projects": [], + "cohort": { "$oid": "616c4b4c649eaa001dd50f83" } + } +]