From 18981a8525aa25c973d4177960c64acd572d1177 Mon Sep 17 00:00:00 2001 From: Sean Date: Thu, 4 Apr 2024 10:30:47 -0400 Subject: [PATCH 01/30] CHE-57 Created forum model --- server/models/forumModel.ts | 12 ++++++++++++ 1 file changed, 12 insertions(+) create mode 100644 server/models/forumModel.ts diff --git a/server/models/forumModel.ts b/server/models/forumModel.ts new file mode 100644 index 0000000..4c69b34 --- /dev/null +++ b/server/models/forumModel.ts @@ -0,0 +1,12 @@ +import mongoose from "mongoose"; + +const forumSchema = new mongoose.Schema({ + title: { type: String, required: true }, + description: { type: String, required: true }, + createdAt: { type: Date, default: Date.now }, + updatedAt: { type: Date, default: Date.now }, +}); + +const Forum = mongoose.model("Forum", forumSchema); + +export default Forum; From 542ca9ef6eb4d34594156e65cfb00d20af8edae8 Mon Sep 17 00:00:00 2001 From: Sean Date: Thu, 4 Apr 2024 10:41:39 -0400 Subject: [PATCH 02/30] CHE-57 Created thread model --- server/models/threadModel.ts | 14 ++++++++++++++ 1 file changed, 14 insertions(+) create mode 100644 server/models/threadModel.ts diff --git a/server/models/threadModel.ts b/server/models/threadModel.ts new file mode 100644 index 0000000..2c4925b --- /dev/null +++ b/server/models/threadModel.ts @@ -0,0 +1,14 @@ +import mongoose from "mongoose"; + +const threadSchema = new mongoose.Schema({ + user: { type: mongoose.Schema.Types.ObjectId, ref: "User", required: true }, // Creator of the thread + forum: { type: mongoose.Schema.Types.ObjectId, ref: "Forum", required: true }, // Associated forum + title: { type: String, required: true }, + content: { type: String, required: true }, + createdAt: { type: Date, default: Date.now }, + updatedAt: { type: Date, default: Date.now }, +}); + +const Thread = mongoose.model("Thread", threadSchema); + +export default Thread; From c1fbe9179860c99c8c7fbb9175dd664e9f37ff9f Mon Sep 17 00:00:00 2001 From: Sean Date: Thu, 4 Apr 2024 10:57:13 -0400 Subject: [PATCH 03/30] CHE-57 Created post model --- server/models/postModel.ts | 17 +++++++++++++++++ server/models/threadModel.ts | 4 ++-- 2 files changed, 19 insertions(+), 2 deletions(-) create mode 100644 server/models/postModel.ts diff --git a/server/models/postModel.ts b/server/models/postModel.ts new file mode 100644 index 0000000..a8b6598 --- /dev/null +++ b/server/models/postModel.ts @@ -0,0 +1,17 @@ +import mongoose from "mongoose"; + +const postSchema = new mongoose.Schema({ + thread: { + type: mongoose.Schema.Types.ObjectId, + ref: "Thread", + required: true, + }, + user: { type: mongoose.Schema.Types.ObjectId, ref: "User", required: true }, + content: { type: String, required: true }, + createdAt: { type: Date, default: Date.now }, + updatedAt: { type: Date, default: Date.now }, +}); + +const Post = mongoose.model("Post", postSchema); + +export default Post; diff --git a/server/models/threadModel.ts b/server/models/threadModel.ts index 2c4925b..3e9c279 100644 --- a/server/models/threadModel.ts +++ b/server/models/threadModel.ts @@ -1,8 +1,8 @@ import mongoose from "mongoose"; const threadSchema = new mongoose.Schema({ - user: { type: mongoose.Schema.Types.ObjectId, ref: "User", required: true }, // Creator of the thread - forum: { type: mongoose.Schema.Types.ObjectId, ref: "Forum", required: true }, // Associated forum + user: { type: mongoose.Schema.Types.ObjectId, ref: "User", required: true }, + forum: { type: mongoose.Schema.Types.ObjectId, ref: "Forum", required: true }, title: { type: String, required: true }, content: { type: String, required: true }, createdAt: { type: Date, default: Date.now }, From b4c758091b93c892b596fab1c10cbc36cb6ed67b Mon Sep 17 00:00:00 2001 From: Sean Date: Thu, 4 Apr 2024 12:07:20 -0400 Subject: [PATCH 04/30] CHE-58 Updated index.ts and added first forum route and controller --- server/controllers/forumController.ts | 29 +++++++++++++++++++++++++++ server/index.ts | 2 ++ server/routes/forumRoutes.ts | 9 +++++++++ 3 files changed, 40 insertions(+) create mode 100644 server/controllers/forumController.ts create mode 100644 server/routes/forumRoutes.ts diff --git a/server/controllers/forumController.ts b/server/controllers/forumController.ts new file mode 100644 index 0000000..3dbb830 --- /dev/null +++ b/server/controllers/forumController.ts @@ -0,0 +1,29 @@ +import Forum from "../models/forumModel"; +import { Request, Response, NextFunction } from "express"; + +// ENDPOINT POST api/forums +// PURPOSE Create a new forum +// ACCESS Admin + +const addForum = async (req: Request, res: Response, next: NextFunction) => { + const { title, description } = req.body; + + try { + //TODO add auth check for admin status + + const forum = await Forum.create({ + title, + description, + }); + + res.status(201).json(forum); + } catch (error) { + next({ + log: `Express error in addForum controller: ${error}`, + status: 500, + message: { err: "Server error creating forum" }, + }); + } +}; + +export { addForum }; diff --git a/server/index.ts b/server/index.ts index a189f85..f930498 100644 --- a/server/index.ts +++ b/server/index.ts @@ -3,6 +3,7 @@ import express, { Request, Response, Application, NextFunction } from "express"; import userRoutes from "./routes/userRoutes"; import profileRoutes from "./routes/profileRoutes"; import authRoutes from "./routes/authRoutes"; +import forumRoutes from "./routes/forumRoutes"; import connectDB from "./config/db"; import dotenv from "dotenv"; import cookieParser from "cookie-parser"; @@ -20,6 +21,7 @@ connectDB(); app.use("/api/users", userRoutes); app.use("/api/profiles", profileRoutes); app.use("/api/auth", authRoutes); +app.use("/api/forums", forumRoutes); console.log(`ENV BEFORE CHECK: ${process.env.NODE_ENV}`); diff --git a/server/routes/forumRoutes.ts b/server/routes/forumRoutes.ts new file mode 100644 index 0000000..8a4c3d0 --- /dev/null +++ b/server/routes/forumRoutes.ts @@ -0,0 +1,9 @@ +import express from "express"; +import { addForum } from "../controllers/forumController"; +import { protect } from "../middleware/authMiddleware"; //TODO Add admin auth middleware + +const router = express.Router(); + +router.post("/", addForum); + +export default router; From 2971a1494e7990685cf29bfcdec8ffd77d1ca773 Mon Sep 17 00:00:00 2001 From: Sean Date: Thu, 4 Apr 2024 13:17:08 -0400 Subject: [PATCH 05/30] CHE-58 Added getAllForums controller and route --- server/controllers/forumController.ts | 23 +++++++++++++++++++++-- server/routes/forumRoutes.ts | 3 ++- 2 files changed, 23 insertions(+), 3 deletions(-) diff --git a/server/controllers/forumController.ts b/server/controllers/forumController.ts index 3dbb830..4adaedb 100644 --- a/server/controllers/forumController.ts +++ b/server/controllers/forumController.ts @@ -4,7 +4,6 @@ import { Request, Response, NextFunction } from "express"; // ENDPOINT POST api/forums // PURPOSE Create a new forum // ACCESS Admin - const addForum = async (req: Request, res: Response, next: NextFunction) => { const { title, description } = req.body; @@ -26,4 +25,24 @@ const addForum = async (req: Request, res: Response, next: NextFunction) => { } }; -export { addForum }; +// ENDPOINT GET api/forums +// PURPOSE Retrieve a list of all forums +// ACCESS all users +const getAllForums = async ( + req: Request, + res: Response, + next: NextFunction +) => { + try { + const forums = await Forum.find({}); + res.status(200).json(forums); + } catch (error) { + next({ + log: `Express error in getAllForums controller: ${error}`, + status: 500, + message: { err: "Server error fetching forums" }, + }); + } +}; + +export { addForum, getAllForums }; diff --git a/server/routes/forumRoutes.ts b/server/routes/forumRoutes.ts index 8a4c3d0..3a6fe2b 100644 --- a/server/routes/forumRoutes.ts +++ b/server/routes/forumRoutes.ts @@ -1,9 +1,10 @@ import express from "express"; -import { addForum } from "../controllers/forumController"; +import { addForum, getAllForums } from "../controllers/forumController"; import { protect } from "../middleware/authMiddleware"; //TODO Add admin auth middleware const router = express.Router(); router.post("/", addForum); +router.get("/", getAllForums); export default router; From 38f4c14e4d430220c41c76ade1aac1494965b58c Mon Sep 17 00:00:00 2001 From: Sean Date: Thu, 4 Apr 2024 13:38:59 -0400 Subject: [PATCH 06/30] CHE-58 Added getForumById controller and route --- server/controllers/forumController.ts | 34 ++++++++++++++++++++++++++- server/routes/forumRoutes.ts | 7 +++++- 2 files changed, 39 insertions(+), 2 deletions(-) diff --git a/server/controllers/forumController.ts b/server/controllers/forumController.ts index 4adaedb..69cb251 100644 --- a/server/controllers/forumController.ts +++ b/server/controllers/forumController.ts @@ -1,4 +1,5 @@ import Forum from "../models/forumModel"; +import Thread from "../models/threadModel"; import { Request, Response, NextFunction } from "express"; // ENDPOINT POST api/forums @@ -45,4 +46,35 @@ const getAllForums = async ( } }; -export { addForum, getAllForums }; +// ENDPOINT GET api/forums/:forumId +// PURPOSE Retrieve a list of all forums +// ACCESS all users +const getForumById = async ( + req: Request, + res: Response, + next: NextFunction +) => { + const { forumId } = req.params; + + try { + const forum = await Forum.findById(forumId); + if (!forum) { + return res.status(404).json({ message: "Forum not found" }); + } + + const threads = await Thread.find({ forum: forumId }).populate( + "user", + "firstName lastName" + ); + + res.status(200).json({ forum, threads }); + } catch (error) { + next({ + log: `Express error in getForumById controller: ${error}`, + status: 500, + message: { err: "Server error fetching forum details" }, + }); + } +}; + +export { addForum, getAllForums, getForumById }; diff --git a/server/routes/forumRoutes.ts b/server/routes/forumRoutes.ts index 3a6fe2b..9e8b5ed 100644 --- a/server/routes/forumRoutes.ts +++ b/server/routes/forumRoutes.ts @@ -1,10 +1,15 @@ import express from "express"; -import { addForum, getAllForums } from "../controllers/forumController"; +import { + addForum, + getAllForums, + getForumById, +} from "../controllers/forumController"; import { protect } from "../middleware/authMiddleware"; //TODO Add admin auth middleware const router = express.Router(); router.post("/", addForum); router.get("/", getAllForums); +router.get("/:forumId", getForumById); export default router; From f74ce600285688402b3563cb2c908fba64559449 Mon Sep 17 00:00:00 2001 From: Sean Date: Thu, 4 Apr 2024 14:11:07 -0400 Subject: [PATCH 07/30] CHE-58 Added updateForum controller and route --- server/controllers/forumController.ts | 32 ++++++++++++++++++++++++++- server/routes/forumRoutes.ts | 2 ++ 2 files changed, 33 insertions(+), 1 deletion(-) diff --git a/server/controllers/forumController.ts b/server/controllers/forumController.ts index 69cb251..3490980 100644 --- a/server/controllers/forumController.ts +++ b/server/controllers/forumController.ts @@ -77,4 +77,34 @@ const getForumById = async ( } }; -export { addForum, getAllForums, getForumById }; +// ENDPOINT PUT api/forums/:forumId +// PURPOSE Update title/description of forum +// ACCESS Admin +const updateForum = async (req: Request, res: Response, next: NextFunction) => { + const { forumId } = req.params; + const { title, description } = req.body; + + try { + //TODO add auth check for admin status + + const forum = await Forum.findByIdAndUpdate( + forumId, + { $set: { title, description } }, + { new: true } + ); + + if (!forum) { + return res.status(404).json({ message: "Forum not found" }); + } + + res.status(200).json(forum); + } catch (error) { + next({ + log: `Express error in updateForum controller: ${error}`, + status: 500, + message: { err: "Server error updating forum details" }, + }); + } +}; + +export { addForum, getAllForums, getForumById, updateForum }; diff --git a/server/routes/forumRoutes.ts b/server/routes/forumRoutes.ts index 9e8b5ed..a4f2621 100644 --- a/server/routes/forumRoutes.ts +++ b/server/routes/forumRoutes.ts @@ -3,6 +3,7 @@ import { addForum, getAllForums, getForumById, + updateForum, } from "../controllers/forumController"; import { protect } from "../middleware/authMiddleware"; //TODO Add admin auth middleware @@ -11,5 +12,6 @@ const router = express.Router(); router.post("/", addForum); router.get("/", getAllForums); router.get("/:forumId", getForumById); +router.put("/:forumId", updateForum); export default router; From 17a9ab83a384c394275c5492fcef8a9660eadaa2 Mon Sep 17 00:00:00 2001 From: Sean Date: Thu, 4 Apr 2024 14:47:22 -0400 Subject: [PATCH 08/30] CHE-58 Added deleteForum controller and route --- server/controllers/forumController.ts | 28 ++++++++++++++++++++++++++- server/routes/forumRoutes.ts | 6 ++++-- 2 files changed, 31 insertions(+), 3 deletions(-) diff --git a/server/controllers/forumController.ts b/server/controllers/forumController.ts index 3490980..0c9d161 100644 --- a/server/controllers/forumController.ts +++ b/server/controllers/forumController.ts @@ -107,4 +107,30 @@ const updateForum = async (req: Request, res: Response, next: NextFunction) => { } }; -export { addForum, getAllForums, getForumById, updateForum }; +// ENDPOINT DELETE api/forums/:forumId +// PURPOSE Delete a forum +// ACCESS Admin +const deleteForum = async (req: Request, res: Response, next: NextFunction) => { + const { forumId } = req.params; + + try { + //TODO add auth check for admin status + + const deletedForum = await Forum.findByIdAndDelete(forumId); + console.log("deletedForum", deletedForum); + + if (!deletedForum) { + return res.status(404).json({ message: "Forum not found" }); + } + + res.status(200).json({ message: "Forum deleted successfully" }); + } catch (error) { + next({ + log: `Express error in deleteForum controller: ${error}`, + status: 500, + message: { err: "Server error deleting forum" }, + }); + } +}; + +export { addForum, getAllForums, getForumById, updateForum, deleteForum }; diff --git a/server/routes/forumRoutes.ts b/server/routes/forumRoutes.ts index a4f2621..82073d7 100644 --- a/server/routes/forumRoutes.ts +++ b/server/routes/forumRoutes.ts @@ -1,6 +1,7 @@ import express from "express"; import { addForum, + deleteForum, getAllForums, getForumById, updateForum, @@ -9,9 +10,10 @@ import { protect } from "../middleware/authMiddleware"; //TODO Add admin auth mi const router = express.Router(); -router.post("/", addForum); +router.post("/", addForum); //TODO Protect with admin auth router.get("/", getAllForums); router.get("/:forumId", getForumById); -router.put("/:forumId", updateForum); +router.put("/:forumId", updateForum); //TODO Protect with admin auth +router.delete("/:forumId", deleteForum); //TODO Protect with admin auth export default router; From d67e581f97c2c277a5be16dbe354166f30faf967 Mon Sep 17 00:00:00 2001 From: Sean Date: Thu, 4 Apr 2024 17:24:04 -0400 Subject: [PATCH 09/30] CHE-58 Added createThread controller and route and added a CustomRequest interface as well as protected some routes --- server/controllers/threadController.ts | 40 ++++++++++++++++++++++++++ server/middleware/authMiddleware.ts | 6 ++-- server/models/userModel.ts | 2 +- server/routes/forumRoutes.ts | 23 +++++++++++---- server/types/customRequest.ts | 9 ++++++ 5 files changed, 71 insertions(+), 9 deletions(-) create mode 100644 server/controllers/threadController.ts create mode 100644 server/types/customRequest.ts diff --git a/server/controllers/threadController.ts b/server/controllers/threadController.ts new file mode 100644 index 0000000..21c1e73 --- /dev/null +++ b/server/controllers/threadController.ts @@ -0,0 +1,40 @@ +import Forum from "../models/forumModel"; +import Thread from "../models/threadModel"; +import { Request, Response, NextFunction } from "express"; +import { CustomRequest } from "../types/customRequest"; + +// ENDPOINT POST api/:forumId/threads +// PURPOSE Create a new thread +// ACCESS Private +const createThread = async ( + req: CustomRequest, + res: Response, + next: NextFunction +) => { + const { forumId } = req.params; + const { title, content } = req.body; + + if (!req.user) { + return res.status(401).json({ message: "Not authenticated" }); + } + const userId = req.user; + + try { + const thread = await Thread.create({ + forum: forumId, + user: userId, + title, + content, + }); + + res.status(201).json(thread); + } catch (error) { + next({ + log: `Express error in createThread controller: ${error}`, + status: 500, + message: { err: "Server error creating thread" }, + }); + } +}; + +export { createThread }; diff --git a/server/middleware/authMiddleware.ts b/server/middleware/authMiddleware.ts index e08abf3..570e3fa 100644 --- a/server/middleware/authMiddleware.ts +++ b/server/middleware/authMiddleware.ts @@ -1,9 +1,9 @@ import jwt from "jsonwebtoken"; import User from "../models/userModel"; import asyncHandler from "express-async-handler"; -import { Request } from "express"; +import { CustomRequest } from "../types/customRequest"; -const protect = asyncHandler(async (req, res, next) => { +const protect = asyncHandler(async (req: CustomRequest, res, next) => { let token; console.log("PROTECT HIT"); console.log(req.headers); @@ -24,7 +24,7 @@ const protect = asyncHandler(async (req, res, next) => { const user = await User.findById(decoded.id).select("-password"); if (!user) throw new Error("User not found"); - + req.user = decoded.id; res.locals.user = user; next(); } catch (error) { diff --git a/server/models/userModel.ts b/server/models/userModel.ts index ffcfcf4..a47edd0 100644 --- a/server/models/userModel.ts +++ b/server/models/userModel.ts @@ -50,6 +50,6 @@ userSchema.pre("save", async function (next) { next(); }); -const User = mongoose.model("users", userSchema); +const User = mongoose.model("User", userSchema); export default User; diff --git a/server/routes/forumRoutes.ts b/server/routes/forumRoutes.ts index 82073d7..68793c5 100644 --- a/server/routes/forumRoutes.ts +++ b/server/routes/forumRoutes.ts @@ -1,4 +1,6 @@ import express from "express"; +import { Request, Response, NextFunction } from "express"; +import { CustomRequest } from "../types/customRequest"; import { addForum, deleteForum, @@ -6,14 +8,25 @@ import { getForumById, updateForum, } from "../controllers/forumController"; + +import { createThread } from "../controllers/threadController"; import { protect } from "../middleware/authMiddleware"; //TODO Add admin auth middleware +function mockAuth(req: CustomRequest, res: Response, next: NextFunction) { + req.user = { + _id: "6569e218784b4454e0b5bda6", + }; + next(); +} + const router = express.Router(); -router.post("/", addForum); //TODO Protect with admin auth -router.get("/", getAllForums); -router.get("/:forumId", getForumById); -router.put("/:forumId", updateForum); //TODO Protect with admin auth -router.delete("/:forumId", deleteForum); //TODO Protect with admin auth +router.post("/", protect, addForum); //TODO Protect with admin auth +router.get("/", protect, getAllForums); +router.get("/:forumId", protect, getForumById); +router.put("/:forumId", protect, updateForum); //TODO Protect with admin auth +router.delete("/:forumId", protect, deleteForum); //TODO Protect with admin auth + +router.post("/:forumId/threads", protect, createThread); export default router; diff --git a/server/types/customRequest.ts b/server/types/customRequest.ts new file mode 100644 index 0000000..38a892b --- /dev/null +++ b/server/types/customRequest.ts @@ -0,0 +1,9 @@ +import { Request } from "express"; + +interface UserPayload { + _id: string; +} + +export interface CustomRequest extends Request { + user?: UserPayload; +} From 38a4c2f835b2a673321dafabb24acf023964337b Mon Sep 17 00:00:00 2001 From: Sean Date: Thu, 4 Apr 2024 17:26:39 -0400 Subject: [PATCH 10/30] CHE-58 Removed unused auth mock and imports --- server/routes/forumRoutes.ts | 9 --------- 1 file changed, 9 deletions(-) diff --git a/server/routes/forumRoutes.ts b/server/routes/forumRoutes.ts index 68793c5..020e02e 100644 --- a/server/routes/forumRoutes.ts +++ b/server/routes/forumRoutes.ts @@ -1,6 +1,4 @@ import express from "express"; -import { Request, Response, NextFunction } from "express"; -import { CustomRequest } from "../types/customRequest"; import { addForum, deleteForum, @@ -12,13 +10,6 @@ import { import { createThread } from "../controllers/threadController"; import { protect } from "../middleware/authMiddleware"; //TODO Add admin auth middleware -function mockAuth(req: CustomRequest, res: Response, next: NextFunction) { - req.user = { - _id: "6569e218784b4454e0b5bda6", - }; - next(); -} - const router = express.Router(); router.post("/", protect, addForum); //TODO Protect with admin auth From 4ea7c5cf1535fa416e7ca6d4a5daa07cbea71ba6 Mon Sep 17 00:00:00 2001 From: Sean Date: Thu, 4 Apr 2024 18:38:08 -0400 Subject: [PATCH 11/30] CHE-58 Added listThreadsByForumId controller and route --- server/controllers/threadController.ts | 27 +++++++++++++++++++++++++- server/routes/forumRoutes.ts | 6 +++++- 2 files changed, 31 insertions(+), 2 deletions(-) diff --git a/server/controllers/threadController.ts b/server/controllers/threadController.ts index 21c1e73..cadc098 100644 --- a/server/controllers/threadController.ts +++ b/server/controllers/threadController.ts @@ -37,4 +37,29 @@ const createThread = async ( } }; -export { createThread }; +// ENDPOINT GET api/:forumId/threads +// PURPOSE Retrieve all threads for a specific forum +// ACCESS Private +const listThreadsByForumId = async ( + req: Request, + res: Response, + next: NextFunction +) => { + const { forumId } = req.params; + + try { + const threads = await Thread.find({ forum: forumId }) + .populate("user", "firstName lastName") + .exec(); + + res.status(200).json(threads); + } catch (error) { + next({ + log: `Express error in listThreadsByForum controller: ${error}`, + status: 500, + message: { err: "Server error listing threads by forum" }, + }); + } +}; + +export { createThread, listThreadsByForumId }; diff --git a/server/routes/forumRoutes.ts b/server/routes/forumRoutes.ts index 020e02e..6ecd2f1 100644 --- a/server/routes/forumRoutes.ts +++ b/server/routes/forumRoutes.ts @@ -7,7 +7,10 @@ import { updateForum, } from "../controllers/forumController"; -import { createThread } from "../controllers/threadController"; +import { + createThread, + listThreadsByForumId, +} from "../controllers/threadController"; import { protect } from "../middleware/authMiddleware"; //TODO Add admin auth middleware const router = express.Router(); @@ -19,5 +22,6 @@ router.put("/:forumId", protect, updateForum); //TODO Protect with admin auth router.delete("/:forumId", protect, deleteForum); //TODO Protect with admin auth router.post("/:forumId/threads", protect, createThread); +router.get("/:forumId/threads", protect, listThreadsByForumId); export default router; From 2d426ba65ea58d4a8dd0a4647d538d640f8a3c01 Mon Sep 17 00:00:00 2001 From: Sean Date: Thu, 4 Apr 2024 19:56:26 -0400 Subject: [PATCH 12/30] CHE-58 Added getThreadById controller and route --- server/controllers/threadController.ts | 37 ++++++++++++++++++++++++-- server/routes/forumRoutes.ts | 2 ++ 2 files changed, 37 insertions(+), 2 deletions(-) diff --git a/server/controllers/threadController.ts b/server/controllers/threadController.ts index cadc098..a03e66d 100644 --- a/server/controllers/threadController.ts +++ b/server/controllers/threadController.ts @@ -1,4 +1,4 @@ -import Forum from "../models/forumModel"; +import Post from "../models/postModel"; import Thread from "../models/threadModel"; import { Request, Response, NextFunction } from "express"; import { CustomRequest } from "../types/customRequest"; @@ -62,4 +62,37 @@ const listThreadsByForumId = async ( } }; -export { createThread, listThreadsByForumId }; +// ENDPOINT GET api/forums/:forumId/threads/:threadId +// PURPOSE Retrieve a specific thread and all of its posts +// ACCESS Private +const getThreadById = async ( + req: Request, + res: Response, + next: NextFunction +) => { + const { forumId, threadId } = req.params; + + try { + const thread = await Thread.findOne({ _id: threadId, forum: forumId }) + .populate("user", "firstName lastName") + .exec(); + + if (!thread) { + return res.status(404).json({ message: "Thread not found" }); + } + + const posts = await Post.find({ thread: threadId }) + .populate("user", "firstName lastName") + .exec(); + + res.status(200).json({ thread, posts }); + } catch (error) { + next({ + log: `Express error in getThreadById controller: ${error}`, + status: 500, + message: { err: "Server error fetching thread details" }, + }); + } +}; + +export { createThread, listThreadsByForumId, getThreadById }; diff --git a/server/routes/forumRoutes.ts b/server/routes/forumRoutes.ts index 6ecd2f1..aa75f91 100644 --- a/server/routes/forumRoutes.ts +++ b/server/routes/forumRoutes.ts @@ -9,6 +9,7 @@ import { import { createThread, + getThreadById, listThreadsByForumId, } from "../controllers/threadController"; import { protect } from "../middleware/authMiddleware"; //TODO Add admin auth middleware @@ -23,5 +24,6 @@ router.delete("/:forumId", protect, deleteForum); //TODO Protect with admin auth router.post("/:forumId/threads", protect, createThread); router.get("/:forumId/threads", protect, listThreadsByForumId); +router.get("/:forumId/threads/:threadId", protect, getThreadById); export default router; From cf9c9a84cc146b3b678cd620cc5cba0c04348b44 Mon Sep 17 00:00:00 2001 From: Sean Date: Fri, 5 Apr 2024 08:41:37 -0400 Subject: [PATCH 13/30] CHE-58 Added updateThread controller and route as well as adjusted auth logic and CustomRequest interface --- server/controllers/threadController.ts | 43 ++++++++++++++++++++++++-- server/middleware/authMiddleware.ts | 2 +- server/routes/forumRoutes.ts | 4 +++ server/types/customRequest.ts | 2 +- 4 files changed, 47 insertions(+), 4 deletions(-) diff --git a/server/controllers/threadController.ts b/server/controllers/threadController.ts index a03e66d..1c2076a 100644 --- a/server/controllers/threadController.ts +++ b/server/controllers/threadController.ts @@ -17,7 +17,7 @@ const createThread = async ( if (!req.user) { return res.status(401).json({ message: "Not authenticated" }); } - const userId = req.user; + const userId = req.user.id; try { const thread = await Thread.create({ @@ -95,4 +95,43 @@ const getThreadById = async ( } }; -export { createThread, listThreadsByForumId, getThreadById }; +// ENDPOINT PUT api/forums/:forumId/threads/:threadId +// PURPOSE Update a specific thread +// ACCESS Private/Admin +const updateThread = async ( + req: CustomRequest, + res: Response, + next: NextFunction +) => { + const { forumId, threadId } = req.params; + const { title, content } = req.body; + + try { + const thread = await Thread.findOne({ _id: threadId, forum: forumId }); + if (!thread) { + return res.status(404).json({ message: "Thread not found" }); + } + + if (!req.user || thread.user._id.toString() !== req.user.id) { + return res + .status(403) + .json({ message: "Not authorized to update this thread" }); + } + + const updatedThread = await Thread.findByIdAndUpdate( + threadId, + { $set: { title, content } }, + { new: true, runValidators: true } + ).populate("user", "firstName lastName"); + + res.status(200).json(updatedThread); + } catch (error) { + next({ + log: `Express error in updateThread controller: ${error}`, + status: 500, + message: { err: "Server error updating thread" }, + }); + } +}; + +export { createThread, listThreadsByForumId, getThreadById, updateThread }; diff --git a/server/middleware/authMiddleware.ts b/server/middleware/authMiddleware.ts index 570e3fa..b399c0e 100644 --- a/server/middleware/authMiddleware.ts +++ b/server/middleware/authMiddleware.ts @@ -24,7 +24,7 @@ const protect = asyncHandler(async (req: CustomRequest, res, next) => { const user = await User.findById(decoded.id).select("-password"); if (!user) throw new Error("User not found"); - req.user = decoded.id; + req.user = { id: user._id.toString() }; res.locals.user = user; next(); } catch (error) { diff --git a/server/routes/forumRoutes.ts b/server/routes/forumRoutes.ts index aa75f91..d6882d7 100644 --- a/server/routes/forumRoutes.ts +++ b/server/routes/forumRoutes.ts @@ -11,19 +11,23 @@ import { createThread, getThreadById, listThreadsByForumId, + updateThread, } from "../controllers/threadController"; import { protect } from "../middleware/authMiddleware"; //TODO Add admin auth middleware const router = express.Router(); +//Forum Routes router.post("/", protect, addForum); //TODO Protect with admin auth router.get("/", protect, getAllForums); router.get("/:forumId", protect, getForumById); router.put("/:forumId", protect, updateForum); //TODO Protect with admin auth router.delete("/:forumId", protect, deleteForum); //TODO Protect with admin auth +//Thread Routes router.post("/:forumId/threads", protect, createThread); router.get("/:forumId/threads", protect, listThreadsByForumId); router.get("/:forumId/threads/:threadId", protect, getThreadById); +router.put("/:forumId/threads/:threadId", protect, updateThread); export default router; diff --git a/server/types/customRequest.ts b/server/types/customRequest.ts index 38a892b..f3d8e34 100644 --- a/server/types/customRequest.ts +++ b/server/types/customRequest.ts @@ -1,7 +1,7 @@ import { Request } from "express"; interface UserPayload { - _id: string; + id: string; } export interface CustomRequest extends Request { From ed5d40ae2ee7cf6f054395154dac7054fcffbf6a Mon Sep 17 00:00:00 2001 From: Sean Date: Fri, 5 Apr 2024 09:28:25 -0400 Subject: [PATCH 14/30] CHE-58 Added deleteThread controller and route --- server/controllers/threadController.ts | 51 +++++++++++++++++++++++++- server/routes/forumRoutes.ts | 4 +- 2 files changed, 53 insertions(+), 2 deletions(-) diff --git a/server/controllers/threadController.ts b/server/controllers/threadController.ts index 1c2076a..cf8eb98 100644 --- a/server/controllers/threadController.ts +++ b/server/controllers/threadController.ts @@ -134,4 +134,53 @@ const updateThread = async ( } }; -export { createThread, listThreadsByForumId, getThreadById, updateThread }; +// ENDPOINT DELETE api/forums/:forumId/threads/:threadId +// PURPOSE Delete a specific thread +// ACCESS Private/Admin +const deleteThread = async ( + req: CustomRequest, + res: Response, + next: NextFunction +) => { + const { forumId, threadId } = req.params; + + try { + const threadToCheck = await Thread.findById(threadId); + if (!threadToCheck) { + return res.status(404).json({ message: "Thread not found" }); + } + //TODO Add admin auth check + if (!req.user || threadToCheck.user.toString() !== req.user.id) { + return res + .status(403) + .json({ message: "Not authorized to delete this thread" }); + } + + const deletedThread = await Thread.findByIdAndDelete({ + _id: threadId, + forum: forumId, + }); + + if (!deletedThread) { + return res + .status(404) + .json({ message: "Thread not found or already deleted" }); + } + + res.status(200).json({ message: "Thread deleted successfully" }); + } catch (error) { + next({ + log: `Express error in deleteThread controller: ${error}`, + status: 500, + message: { err: "Server error deleting thread" }, + }); + } +}; + +export { + createThread, + listThreadsByForumId, + getThreadById, + updateThread, + deleteThread, +}; diff --git a/server/routes/forumRoutes.ts b/server/routes/forumRoutes.ts index d6882d7..8095254 100644 --- a/server/routes/forumRoutes.ts +++ b/server/routes/forumRoutes.ts @@ -9,6 +9,7 @@ import { import { createThread, + deleteThread, getThreadById, listThreadsByForumId, updateThread, @@ -28,6 +29,7 @@ router.delete("/:forumId", protect, deleteForum); //TODO Protect with admin auth router.post("/:forumId/threads", protect, createThread); router.get("/:forumId/threads", protect, listThreadsByForumId); router.get("/:forumId/threads/:threadId", protect, getThreadById); -router.put("/:forumId/threads/:threadId", protect, updateThread); +router.put("/:forumId/threads/:threadId", protect, updateThread); //TODO Protect with admin auth +router.delete("/:forumId/threads/:threadId", protect, deleteThread); //TODO Protect with admin auth export default router; From 99e01520ecd83b02b56c73474f0d65125ceabc1c Mon Sep 17 00:00:00 2001 From: Sean Date: Fri, 5 Apr 2024 10:40:47 -0400 Subject: [PATCH 15/30] CHE-58 Added listPostsByThread controller and route --- server/controllers/postController.ts | 29 ++++++++++++++++++++++++++++ server/routes/forumRoutes.ts | 6 ++++++ 2 files changed, 35 insertions(+) create mode 100644 server/controllers/postController.ts diff --git a/server/controllers/postController.ts b/server/controllers/postController.ts new file mode 100644 index 0000000..238db6b --- /dev/null +++ b/server/controllers/postController.ts @@ -0,0 +1,29 @@ +import Post from "../models/postModel"; +import { Request, Response, NextFunction } from "express"; + +// ENDPOINT GET api/forums/:forumId/threads/:threadId/posts +// PURPOSE Retrieve all posts from a specific thread +// ACCESS Private +const listPostsByThreadId = async ( + req: Request, + res: Response, + next: NextFunction +) => { + const { threadId } = req.params; + + try { + const posts = await Post.find({ thread: threadId }) + .populate("user", "firstName lastName") + .exec(); + + res.status(200).json(posts); + } catch (error) { + next({ + log: `Express error in listPostsByThreadId controller: ${error}`, + status: 500, + message: { err: "Server error fetching posts" }, + }); + } +}; + +export { listPostsByThreadId }; diff --git a/server/routes/forumRoutes.ts b/server/routes/forumRoutes.ts index 8095254..f7664c5 100644 --- a/server/routes/forumRoutes.ts +++ b/server/routes/forumRoutes.ts @@ -14,6 +14,9 @@ import { listThreadsByForumId, updateThread, } from "../controllers/threadController"; + +import { listPostsByThreadId } from "../controllers/postController"; + import { protect } from "../middleware/authMiddleware"; //TODO Add admin auth middleware const router = express.Router(); @@ -32,4 +35,7 @@ router.get("/:forumId/threads/:threadId", protect, getThreadById); router.put("/:forumId/threads/:threadId", protect, updateThread); //TODO Protect with admin auth router.delete("/:forumId/threads/:threadId", protect, deleteThread); //TODO Protect with admin auth +//Post Routes +router.get("/:forumId/threads/:threadId/posts", protect, listPostsByThreadId); + export default router; From 9145261dd55942067e686f77d92d050b7300f783 Mon Sep 17 00:00:00 2001 From: Sean Date: Fri, 5 Apr 2024 11:09:50 -0400 Subject: [PATCH 16/30] CHE-58 Added createPost controller and route --- server/controllers/postController.ts | 41 +++++++++++++++++++++++++++- server/routes/forumRoutes.ts | 3 +- 2 files changed, 42 insertions(+), 2 deletions(-) diff --git a/server/controllers/postController.ts b/server/controllers/postController.ts index 238db6b..580d6db 100644 --- a/server/controllers/postController.ts +++ b/server/controllers/postController.ts @@ -1,5 +1,7 @@ import Post from "../models/postModel"; +import Thread from "../models/threadModel"; import { Request, Response, NextFunction } from "express"; +import { CustomRequest } from "../types/customRequest"; // ENDPOINT GET api/forums/:forumId/threads/:threadId/posts // PURPOSE Retrieve all posts from a specific thread @@ -26,4 +28,41 @@ const listPostsByThreadId = async ( } }; -export { listPostsByThreadId }; +// ENDPOINT POST api/forums/:forumId/threads/:threadId/posts +// PURPOSE Create a new post on thread +// ACCESS Private +const createPost = async ( + req: CustomRequest, + res: Response, + next: NextFunction +) => { + const { threadId } = req.params; + const { content } = req.body; + + if (!req.user || !req.user.id) { + return res.status(401).json({ message: "Not authenticated" }); + } + + try { + const threadExists = await Thread.findById(threadId); + if (!threadExists) { + return res.status(404).json({ message: "Thread not found" }); + } + + const newPost = await Post.create({ + thread: threadId, + user: req.user.id, + content, + }); + + res.status(201).json(newPost); + } catch (error) { + next({ + log: `Express error in createPost controller: ${error}`, + status: 500, + message: { err: "Server error creating post" }, + }); + } +}; + +export { listPostsByThreadId, createPost }; diff --git a/server/routes/forumRoutes.ts b/server/routes/forumRoutes.ts index f7664c5..4e274d2 100644 --- a/server/routes/forumRoutes.ts +++ b/server/routes/forumRoutes.ts @@ -15,7 +15,7 @@ import { updateThread, } from "../controllers/threadController"; -import { listPostsByThreadId } from "../controllers/postController"; +import { listPostsByThreadId, createPost } from "../controllers/postController"; import { protect } from "../middleware/authMiddleware"; //TODO Add admin auth middleware @@ -37,5 +37,6 @@ router.delete("/:forumId/threads/:threadId", protect, deleteThread); //TODO Prot //Post Routes router.get("/:forumId/threads/:threadId/posts", protect, listPostsByThreadId); +router.post("/:forumId/threads/:threadId/posts", protect, createPost); export default router; From 900ed1997790cd2de3448996100abd84291bdc11 Mon Sep 17 00:00:00 2001 From: Sean Date: Fri, 5 Apr 2024 11:39:59 -0400 Subject: [PATCH 17/30] CHE-58 Added updatePost controller and route --- server/controllers/postController.ts | 47 +++++++++++++++++++++++++++- server/routes/forumRoutes.ts | 7 ++++- 2 files changed, 52 insertions(+), 2 deletions(-) diff --git a/server/controllers/postController.ts b/server/controllers/postController.ts index 580d6db..d1cf69b 100644 --- a/server/controllers/postController.ts +++ b/server/controllers/postController.ts @@ -65,4 +65,49 @@ const createPost = async ( } }; -export { listPostsByThreadId, createPost }; +// ENDPOINT PUT api/forums/:forumId/threads/:threadId/:postId +// PURPOSE Update an existing post +// ACCESS Private +const updatePost = async ( + req: CustomRequest, + res: Response, + next: NextFunction +) => { + const { postId } = req.params; + const { content } = req.body; + + try { + const postToCheck = await Post.findById(postId).populate("user"); + if (!postToCheck) { + return res.status(404).json({ message: "Post not found" }); + } + + if (!req.user || postToCheck.user._id.toString() !== req.user.id) { + return res + .status(403) + .json({ message: "Not authorized to update this post" }); + } + + const updatedPost = await Post.findByIdAndUpdate( + postId, + { $set: { content } }, + { new: true, runValidators: true } + ).populate("user", "firstName lastName"); + + if (!updatedPost) { + return res + .status(404) + .json({ message: "Unable to update post or post not found" }); + } + + res.status(200).json(updatedPost); + } catch (error) { + next({ + log: `Express error in updatePost controller: ${error}`, + status: 500, + message: { err: "Server error updating post" }, + }); + } +}; + +export { listPostsByThreadId, createPost, updatePost }; diff --git a/server/routes/forumRoutes.ts b/server/routes/forumRoutes.ts index 4e274d2..fad14b2 100644 --- a/server/routes/forumRoutes.ts +++ b/server/routes/forumRoutes.ts @@ -15,7 +15,11 @@ import { updateThread, } from "../controllers/threadController"; -import { listPostsByThreadId, createPost } from "../controllers/postController"; +import { + listPostsByThreadId, + createPost, + updatePost, +} from "../controllers/postController"; import { protect } from "../middleware/authMiddleware"; //TODO Add admin auth middleware @@ -38,5 +42,6 @@ router.delete("/:forumId/threads/:threadId", protect, deleteThread); //TODO Prot //Post Routes router.get("/:forumId/threads/:threadId/posts", protect, listPostsByThreadId); router.post("/:forumId/threads/:threadId/posts", protect, createPost); +router.put("/:forumId/threads/:threadId/posts/:postId", protect, updatePost); export default router; From 9567e0b50f28ff4e1c3b20f01a6f13f75c294ce6 Mon Sep 17 00:00:00 2001 From: Sean Date: Fri, 5 Apr 2024 12:00:09 -0400 Subject: [PATCH 18/30] CHE-58 Added deletePost controller and route --- server/controllers/postController.ts | 43 +++++++++++++++++++++++++++- server/routes/forumRoutes.ts | 4 ++- 2 files changed, 45 insertions(+), 2 deletions(-) diff --git a/server/controllers/postController.ts b/server/controllers/postController.ts index d1cf69b..54084fc 100644 --- a/server/controllers/postController.ts +++ b/server/controllers/postController.ts @@ -110,4 +110,45 @@ const updatePost = async ( } }; -export { listPostsByThreadId, createPost, updatePost }; +// ENDPOINT DELETE api/forums/:forumId/threads/:threadId/:postId +// PURPOSE Delete an existing post +// ACCESS Private, Admin +const deletePost = async ( + req: CustomRequest, + res: Response, + next: NextFunction +) => { + const { postId } = req.params; + + try { + const postToCheck = await Post.findById(postId).populate("user"); + if (!postToCheck) { + return res.status(404).json({ message: "Post not found" }); + } + + //TODO Add admin rights to delete posts for Jimmy + if (!req.user || postToCheck.user._id.toString() !== req.user.id) { + return res + .status(403) + .json({ message: "Not authorized to delete this post" }); + } + + const deletedPost = await Post.findByIdAndDelete(postId); + + if (!deletedPost) { + return res + .status(404) + .json({ message: "Post not found or already deleted" }); + } + + res.status(200).json({ message: "Post deleted successfully" }); + } catch (error) { + next({ + log: `Express error in deletePost controller: ${error}`, + status: 500, + message: { err: "Server error deleting post" }, + }); + } +}; + +export { listPostsByThreadId, createPost, updatePost, deletePost }; diff --git a/server/routes/forumRoutes.ts b/server/routes/forumRoutes.ts index fad14b2..6d9ab3b 100644 --- a/server/routes/forumRoutes.ts +++ b/server/routes/forumRoutes.ts @@ -19,6 +19,7 @@ import { listPostsByThreadId, createPost, updatePost, + deletePost, } from "../controllers/postController"; import { protect } from "../middleware/authMiddleware"; //TODO Add admin auth middleware @@ -36,12 +37,13 @@ router.delete("/:forumId", protect, deleteForum); //TODO Protect with admin auth router.post("/:forumId/threads", protect, createThread); router.get("/:forumId/threads", protect, listThreadsByForumId); router.get("/:forumId/threads/:threadId", protect, getThreadById); -router.put("/:forumId/threads/:threadId", protect, updateThread); //TODO Protect with admin auth +router.put("/:forumId/threads/:threadId", protect, updateThread); router.delete("/:forumId/threads/:threadId", protect, deleteThread); //TODO Protect with admin auth //Post Routes router.get("/:forumId/threads/:threadId/posts", protect, listPostsByThreadId); router.post("/:forumId/threads/:threadId/posts", protect, createPost); router.put("/:forumId/threads/:threadId/posts/:postId", protect, updatePost); +router.delete("/:forumId/threads/:threadId/posts/:postId", protect, deletePost); //TODO Protect with admin auth export default router; From 5f92deb0da80223bad6bfa2c338c54c97c285693 Mon Sep 17 00:00:00 2001 From: Sean Date: Fri, 19 Apr 2024 10:21:43 -0400 Subject: [PATCH 19/30] CHE-62 Created a basic ForumsList component to display forums --- .../Forums/ForumsList/ForumsList.tsx | 49 +++++++++++++++++++ client/src/pages/Forums/Forums.tsx | 2 + 2 files changed, 51 insertions(+) create mode 100644 client/src/components/Forums/ForumsList/ForumsList.tsx diff --git a/client/src/components/Forums/ForumsList/ForumsList.tsx b/client/src/components/Forums/ForumsList/ForumsList.tsx new file mode 100644 index 0000000..b053aca --- /dev/null +++ b/client/src/components/Forums/ForumsList/ForumsList.tsx @@ -0,0 +1,49 @@ +import React, { useEffect, useState } from "react"; +import axios from "axios"; + +interface Forum { + _id: string; + title: string; + description?: string; +} + +const ForumsList = () => { + const [forums, setForums] = useState([]); + const [loading, setLoading] = useState(false); + const [error, setError] = useState(null); + + useEffect(() => { + const fetchForums = async () => { + setLoading(true); + try { + const { data } = await axios.get("/api/forums", { + withCredentials: true, + }); + setForums(data); + setLoading(false); + } catch (err) { + const error = err as Error; + setError(error.message); + setLoading(false); + } + }; + + fetchForums(); + }, []); + + if (loading) return
Loading...
; + if (error) return
Error: {error}
; + + return ( +
+

Forums

+
    + {forums.map((forum) => ( +
  • {forum.title}
  • + ))} +
+
+ ); +}; + +export default ForumsList; diff --git a/client/src/pages/Forums/Forums.tsx b/client/src/pages/Forums/Forums.tsx index 5e53c40..41d09cc 100644 --- a/client/src/pages/Forums/Forums.tsx +++ b/client/src/pages/Forums/Forums.tsx @@ -1,9 +1,11 @@ import React from "react"; +import ForumsList from "../../components/Forums/ForumsList/ForumsList"; const Forums = (): JSX.Element => { return (

FORUMS

+
); }; From 5c733e71512c8dae639898532c7cb8cd4f7f69ea Mon Sep 17 00:00:00 2001 From: Sean Date: Sat, 20 Apr 2024 08:02:49 -0400 Subject: [PATCH 20/30] CHE-62 Functionality and some styling for Forums, ThreadsDisplay, and ForumsList components. Controllers and Routes updated --- .../Forums/ForumsList/ForumsList.tsx | 30 ++++++++- .../Forums/ThreadsDisplay/ThreadsDisplay.tsx | 62 +++++++++++++++++++ client/src/pages/Forums/Forums.tsx | 22 +++++-- client/types/forums.ts | 15 +++++ server/controllers/threadController.ts | 23 +++++++ server/routes/forumRoutes.ts | 4 ++ 6 files changed, 149 insertions(+), 7 deletions(-) create mode 100644 client/src/components/Forums/ThreadsDisplay/ThreadsDisplay.tsx create mode 100644 client/types/forums.ts diff --git a/client/src/components/Forums/ForumsList/ForumsList.tsx b/client/src/components/Forums/ForumsList/ForumsList.tsx index b053aca..a0fcdbd 100644 --- a/client/src/components/Forums/ForumsList/ForumsList.tsx +++ b/client/src/components/Forums/ForumsList/ForumsList.tsx @@ -7,7 +7,15 @@ interface Forum { description?: string; } -const ForumsList = () => { +interface ForumsListProps { + onForumSelect: (forumId: string | null) => void; + selectedForumId: string | null; +} + +const ForumsList: React.FC = ({ + onForumSelect, + selectedForumId, +}) => { const [forums, setForums] = useState([]); const [loading, setLoading] = useState(false); const [error, setError] = useState(null); @@ -36,10 +44,26 @@ const ForumsList = () => { return (
-

Forums

+

Forums

    +
  • onForumSelect(null)} + className={`cursor-pointer p-2 hover:bg-gray-800 rounded-md ${ + selectedForumId === null ? "bg-gray-700" : "" + }`} + > + All Forums +
  • {forums.map((forum) => ( -
  • {forum.title}
  • +
  • onForumSelect(forum._id)} + className={`cursor-pointer p-2 hover:bg-gray-800 rounded-md ${ + selectedForumId === forum._id ? "bg-gray-700" : "" + }`} + > + {forum.title} +
  • ))}
diff --git a/client/src/components/Forums/ThreadsDisplay/ThreadsDisplay.tsx b/client/src/components/Forums/ThreadsDisplay/ThreadsDisplay.tsx new file mode 100644 index 0000000..153c12a --- /dev/null +++ b/client/src/components/Forums/ThreadsDisplay/ThreadsDisplay.tsx @@ -0,0 +1,62 @@ +import React, { useEffect, useState } from "react"; +import axios from "axios"; +import { Thread } from "../../../../types/forums"; + +interface ThreadsDisplayProps { + forumId?: string | null; +} + +const ThreadsDisplay: React.FC = ({ forumId }) => { + const [threads, setThreads] = useState([]); + const [loading, setLoading] = useState(false); + const [error, setError] = useState(null); + + useEffect(() => { + const fetchThreads = async () => { + setLoading(true); + try { + const endpoint = forumId + ? `/api/forums/${forumId}/threads` + : "/api/forums/threads"; + const { data } = await axios.get(endpoint, { + withCredentials: true, + }); + setThreads(data); + setLoading(false); + } catch (err) { + const error = err as Error; + setError(error.message); + setLoading(false); + } + }; + + fetchThreads(); + }, [forumId]); + + if (loading) return
Loading...
; + if (error) return
Error: {error}
; + + return ( +
+

+ {forumId + ? `Showing threads for forum ${forumId}` + : "Showing all threads"} +

+
    + {threads.map((thread) => ( +
  • +

    {thread.title}

    +

    {thread.content}

    + + Started by {thread.user.firstName} on{" "} + {new Date(thread.createdAt).toLocaleDateString()} + +
  • + ))} +
+
+ ); +}; + +export default ThreadsDisplay; diff --git a/client/src/pages/Forums/Forums.tsx b/client/src/pages/Forums/Forums.tsx index 41d09cc..8d865c3 100644 --- a/client/src/pages/Forums/Forums.tsx +++ b/client/src/pages/Forums/Forums.tsx @@ -1,11 +1,25 @@ -import React from "react"; +import React, { useState } from "react"; import ForumsList from "../../components/Forums/ForumsList/ForumsList"; +import ThreadsDisplay from "../../components/Forums/ThreadsDisplay/ThreadsDisplay"; const Forums = (): JSX.Element => { + const [selectedForumId, setSelectedForumId] = useState(null); + + const handleForumSelect = (forumId: string | null) => { + setSelectedForumId(forumId); + }; return ( -
-

FORUMS

- +
+
+ +
+ +
+ +
); }; diff --git a/client/types/forums.ts b/client/types/forums.ts new file mode 100644 index 0000000..59e5891 --- /dev/null +++ b/client/types/forums.ts @@ -0,0 +1,15 @@ +export interface IUser { + firstName: string; + lastName: string; + _id: string; + email: string; + token: string; +} + +export interface Thread { + _id: string; + title: string; + content: string; + user: IUser; + createdAt: string; +} diff --git a/server/controllers/threadController.ts b/server/controllers/threadController.ts index cf8eb98..854cb70 100644 --- a/server/controllers/threadController.ts +++ b/server/controllers/threadController.ts @@ -37,6 +37,28 @@ const createThread = async ( } }; +// ENDPOINT GET api/threads +// PURPOSE Retrieve all threads +// ACCESS Private +const getAllThreads = async ( + req: CustomRequest, + res: Response, + next: NextFunction +) => { + try { + const threads = await Thread.find({}) + .populate("user", "firstName lastName") + .exec(); + res.status(200).json(threads); + } catch (error) { + next({ + log: `Express error in getAllThreads controller: ${error}`, + status: 500, + message: { err: "Server error fetching all threads" }, + }); + } +}; + // ENDPOINT GET api/:forumId/threads // PURPOSE Retrieve all threads for a specific forum // ACCESS Private @@ -183,4 +205,5 @@ export { getThreadById, updateThread, deleteThread, + getAllThreads, }; diff --git a/server/routes/forumRoutes.ts b/server/routes/forumRoutes.ts index 6d9ab3b..f1a3e66 100644 --- a/server/routes/forumRoutes.ts +++ b/server/routes/forumRoutes.ts @@ -13,6 +13,7 @@ import { getThreadById, listThreadsByForumId, updateThread, + getAllThreads, } from "../controllers/threadController"; import { @@ -26,6 +27,8 @@ import { protect } from "../middleware/authMiddleware"; //TODO Add admin auth mi const router = express.Router(); +router.get("/threads", getAllThreads); + //Forum Routes router.post("/", protect, addForum); //TODO Protect with admin auth router.get("/", protect, getAllForums); @@ -34,6 +37,7 @@ router.put("/:forumId", protect, updateForum); //TODO Protect with admin auth router.delete("/:forumId", protect, deleteForum); //TODO Protect with admin auth //Thread Routes + router.post("/:forumId/threads", protect, createThread); router.get("/:forumId/threads", protect, listThreadsByForumId); router.get("/:forumId/threads/:threadId", protect, getThreadById); From 9d80563a989f14011ceffdd2e5520b79b98a7a88 Mon Sep 17 00:00:00 2001 From: Sean Date: Sat, 20 Apr 2024 08:18:14 -0400 Subject: [PATCH 21/30] CHE-62 Reset snapshot --- .../Forums/__snapshots__/Forums.test.tsx.snap | 15 --------------- 1 file changed, 15 deletions(-) delete mode 100644 client/src/pages/Forums/__snapshots__/Forums.test.tsx.snap diff --git a/client/src/pages/Forums/__snapshots__/Forums.test.tsx.snap b/client/src/pages/Forums/__snapshots__/Forums.test.tsx.snap deleted file mode 100644 index a0da421..0000000 --- a/client/src/pages/Forums/__snapshots__/Forums.test.tsx.snap +++ /dev/null @@ -1,15 +0,0 @@ -// Jest Snapshot v1, https://goo.gl/fbAQLP - -exports[`Forums Page matches the snapshot 1`] = ` - -
-

- FORUMS -

-
-
-`; From 5e468762dc86f0f41ebe18d511bb5f20ae42d565 Mon Sep 17 00:00:00 2001 From: Sean Date: Sat, 20 Apr 2024 08:26:20 -0400 Subject: [PATCH 22/30] CHE-62 Simplified testing file to a basic render test. --- client/src/pages/Forums/Forums.test.tsx | 11 ++--------- 1 file changed, 2 insertions(+), 9 deletions(-) diff --git a/client/src/pages/Forums/Forums.test.tsx b/client/src/pages/Forums/Forums.test.tsx index 6785aa2..4978cb2 100644 --- a/client/src/pages/Forums/Forums.test.tsx +++ b/client/src/pages/Forums/Forums.test.tsx @@ -4,14 +4,7 @@ import "@testing-library/jest-dom"; import Forums from "./Forums"; describe("Forums Page", () => { - it("renders the test H1 correctly", () => { - const { getByText } = render(); - const title = getByText("FORUMS"); - expect(title).toBeInTheDocument(); - }); - - it("matches the snapshot", () => { - const { asFragment } = render(); - expect(asFragment()).toMatchSnapshot(); + it("renders without crashing", () => { + render(); }); }); From 29291c49b5fb800528bddc6c18823d4f51b0f08c Mon Sep 17 00:00:00 2001 From: Sean Date: Sat, 20 Apr 2024 09:33:01 -0400 Subject: [PATCH 23/30] Quick refactors and updates to allow the display of the Forum title in the ThreadsDisplay component --- .../Forums/ThreadsDisplay/ThreadsDisplay.tsx | 21 ++++++++++++------- client/types/forums.ts | 5 +++++ server/controllers/forumController.ts | 2 +- 3 files changed, 19 insertions(+), 9 deletions(-) diff --git a/client/src/components/Forums/ThreadsDisplay/ThreadsDisplay.tsx b/client/src/components/Forums/ThreadsDisplay/ThreadsDisplay.tsx index 153c12a..e539035 100644 --- a/client/src/components/Forums/ThreadsDisplay/ThreadsDisplay.tsx +++ b/client/src/components/Forums/ThreadsDisplay/ThreadsDisplay.tsx @@ -1,6 +1,6 @@ import React, { useEffect, useState } from "react"; import axios from "axios"; -import { Thread } from "../../../../types/forums"; +import { Thread, IForum } from "../../../../types/forums"; interface ThreadsDisplayProps { forumId?: string | null; @@ -8,20 +8,27 @@ interface ThreadsDisplayProps { const ThreadsDisplay: React.FC = ({ forumId }) => { const [threads, setThreads] = useState([]); + const [forum, setForum] = useState(null); const [loading, setLoading] = useState(false); const [error, setError] = useState(null); useEffect(() => { - const fetchThreads = async () => { + const fetchForumAndThreads = async () => { setLoading(true); try { const endpoint = forumId - ? `/api/forums/${forumId}/threads` + ? `/api/forums/${forumId}` : "/api/forums/threads"; const { data } = await axios.get(endpoint, { withCredentials: true, }); - setThreads(data); + if (forumId) { + setForum(data.forum); + setThreads(data.threads); + } else { + setForum(null); + setThreads(data); + } setLoading(false); } catch (err) { const error = err as Error; @@ -30,7 +37,7 @@ const ThreadsDisplay: React.FC = ({ forumId }) => { } }; - fetchThreads(); + fetchForumAndThreads(); }, [forumId]); if (loading) return
Loading...
; @@ -39,9 +46,7 @@ const ThreadsDisplay: React.FC = ({ forumId }) => { return (

- {forumId - ? `Showing threads for forum ${forumId}` - : "Showing all threads"} + {forum ? `Showing threads for ${forum.title}` : "Showing all threads"}

    {threads.map((thread) => ( diff --git a/client/types/forums.ts b/client/types/forums.ts index 59e5891..32ff520 100644 --- a/client/types/forums.ts +++ b/client/types/forums.ts @@ -13,3 +13,8 @@ export interface Thread { user: IUser; createdAt: string; } + +export interface IForum { + _id: string; + title: string; +} diff --git a/server/controllers/forumController.ts b/server/controllers/forumController.ts index 0c9d161..41db712 100644 --- a/server/controllers/forumController.ts +++ b/server/controllers/forumController.ts @@ -47,7 +47,7 @@ const getAllForums = async ( }; // ENDPOINT GET api/forums/:forumId -// PURPOSE Retrieve a list of all forums +// PURPOSE Retrieve a specific forum and its threads // ACCESS all users const getForumById = async ( req: Request, From 3ead5199bd0279e1803eea1f4c3a208374588ad5 Mon Sep 17 00:00:00 2001 From: Brok3Turtl3 Date: Sat, 20 Apr 2024 12:32:21 -0400 Subject: [PATCH 24/30] CHE-64 Added a Post type and created basic ThreadDetail component --- .../Forums/ThreadDetails/ThreadDetails.tsx | 63 +++++++++++++++++++ client/types/forums.ts | 7 +++ 2 files changed, 70 insertions(+) create mode 100644 client/src/components/Forums/ThreadDetails/ThreadDetails.tsx diff --git a/client/src/components/Forums/ThreadDetails/ThreadDetails.tsx b/client/src/components/Forums/ThreadDetails/ThreadDetails.tsx new file mode 100644 index 0000000..04200db --- /dev/null +++ b/client/src/components/Forums/ThreadDetails/ThreadDetails.tsx @@ -0,0 +1,63 @@ +import React, { useEffect, useState } from "react"; +import axios from "axios"; +import { Thread, IPost } from "../../../../types/forums"; + +interface ThreadDetailProps { + forumId: string; + threadId: string; +} + +const ThreadDetail: React.FC = ({ forumId, threadId }) => { + const [thread, setThread] = useState(null); + const [posts, setPosts] = useState([]); + const [loading, setLoading] = useState(false); + const [error, setError] = useState(null); + + useEffect(() => { + const fetchThreadDetails = async () => { + setLoading(true); + try { + const response = await axios.get( + `/api/forums/${forumId}/threads/${threadId}`, + { + withCredentials: true, + } + ); + setThread(response.data.thread); + setPosts(response.data.posts); + setLoading(false); + } catch (err) { + const error = err as Error; + setError(error.message); + setLoading(false); + } + }; + + fetchThreadDetails(); + }, [forumId, threadId]); + + if (loading) return
    Loading...
    ; + if (error) return
    Error: {error}
    ; + if (!thread) return
    Thread not found.
    ; + + return ( +
    +

    {thread.title}

    +

    {thread.content}

    +
    +

    Replies

    + {posts.map((post) => ( +
    +

    {post.content}

    + + By {post.user.firstName} {post.user.lastName} on{" "} + {new Date(post.createdAt).toLocaleDateString()} + +
    + ))} +
    +
    + ); +}; + +export default ThreadDetail; diff --git a/client/types/forums.ts b/client/types/forums.ts index 32ff520..496cfea 100644 --- a/client/types/forums.ts +++ b/client/types/forums.ts @@ -18,3 +18,10 @@ export interface IForum { _id: string; title: string; } + +export interface IPost { + _id: string; + user: IUser; + content: string; + createdAt: string; +} From 1d49f9f1822a4488e928cd9f333ea7603bb716e8 Mon Sep 17 00:00:00 2001 From: Brok3Turtl3 Date: Sat, 20 Apr 2024 16:09:23 -0400 Subject: [PATCH 25/30] CHE-64 Functionality for displaying posts on threads in place --- .../Forums/ThreadDetails/ThreadDetails.tsx | 15 +++++++------- .../Forums/ThreadsDisplay/ThreadsDisplay.tsx | 12 +++++++++-- client/src/pages/Forums/Forums.tsx | 20 ++++++++++++++++++- server/controllers/threadController.ts | 4 ++-- server/routes/forumRoutes.ts | 1 + 5 files changed, 39 insertions(+), 13 deletions(-) diff --git a/client/src/components/Forums/ThreadDetails/ThreadDetails.tsx b/client/src/components/Forums/ThreadDetails/ThreadDetails.tsx index 04200db..253076d 100644 --- a/client/src/components/Forums/ThreadDetails/ThreadDetails.tsx +++ b/client/src/components/Forums/ThreadDetails/ThreadDetails.tsx @@ -3,7 +3,7 @@ import axios from "axios"; import { Thread, IPost } from "../../../../types/forums"; interface ThreadDetailProps { - forumId: string; + forumId: string | null; threadId: string; } @@ -17,14 +17,13 @@ const ThreadDetail: React.FC = ({ forumId, threadId }) => { const fetchThreadDetails = async () => { setLoading(true); try { - const response = await axios.get( - `/api/forums/${forumId}/threads/${threadId}`, - { - withCredentials: true, - } - ); + const endpoint = forumId + ? `/api/forums/${forumId}/threads/${threadId}` + : `/api/forums/threads/${threadId}`; + const response = await axios.get(endpoint, { withCredentials: true }); setThread(response.data.thread); - setPosts(response.data.posts); + setPosts(response.data.posts || []); + setLoading(false); } catch (err) { const error = err as Error; diff --git a/client/src/components/Forums/ThreadsDisplay/ThreadsDisplay.tsx b/client/src/components/Forums/ThreadsDisplay/ThreadsDisplay.tsx index e539035..566474a 100644 --- a/client/src/components/Forums/ThreadsDisplay/ThreadsDisplay.tsx +++ b/client/src/components/Forums/ThreadsDisplay/ThreadsDisplay.tsx @@ -4,9 +4,13 @@ import { Thread, IForum } from "../../../../types/forums"; interface ThreadsDisplayProps { forumId?: string | null; + onThreadSelect: (threadId: string) => void; } -const ThreadsDisplay: React.FC = ({ forumId }) => { +const ThreadsDisplay: React.FC = ({ + forumId, + onThreadSelect, +}) => { const [threads, setThreads] = useState([]); const [forum, setForum] = useState(null); const [loading, setLoading] = useState(false); @@ -50,7 +54,11 @@ const ThreadsDisplay: React.FC = ({ forumId }) => {
      {threads.map((thread) => ( -
    • +
    • onThreadSelect(thread._id)} + >

      {thread.title}

      {thread.content}

      diff --git a/client/src/pages/Forums/Forums.tsx b/client/src/pages/Forums/Forums.tsx index 8d865c3..9d32b4f 100644 --- a/client/src/pages/Forums/Forums.tsx +++ b/client/src/pages/Forums/Forums.tsx @@ -1,13 +1,21 @@ import React, { useState } from "react"; import ForumsList from "../../components/Forums/ForumsList/ForumsList"; import ThreadsDisplay from "../../components/Forums/ThreadsDisplay/ThreadsDisplay"; +import ThreadDetails from "../../components/Forums/ThreadDetails/ThreadDetails"; const Forums = (): JSX.Element => { const [selectedForumId, setSelectedForumId] = useState(null); + const [selectedThreadId, setSelectedThreadId] = useState(null); const handleForumSelect = (forumId: string | null) => { setSelectedForumId(forumId); + setSelectedThreadId(null); }; + + const handleThreadSelect = (threadId: string) => { + setSelectedThreadId(threadId); + }; + return (
      @@ -18,7 +26,17 @@ const Forums = (): JSX.Element => {
      - + {selectedThreadId ? ( + + ) : ( + + )}
      ); diff --git a/server/controllers/threadController.ts b/server/controllers/threadController.ts index 854cb70..fd3009b 100644 --- a/server/controllers/threadController.ts +++ b/server/controllers/threadController.ts @@ -92,10 +92,10 @@ const getThreadById = async ( res: Response, next: NextFunction ) => { - const { forumId, threadId } = req.params; + const { threadId } = req.params; try { - const thread = await Thread.findOne({ _id: threadId, forum: forumId }) + const thread = await Thread.findOne({ _id: threadId }) .populate("user", "firstName lastName") .exec(); diff --git a/server/routes/forumRoutes.ts b/server/routes/forumRoutes.ts index f1a3e66..105ec55 100644 --- a/server/routes/forumRoutes.ts +++ b/server/routes/forumRoutes.ts @@ -28,6 +28,7 @@ import { protect } from "../middleware/authMiddleware"; //TODO Add admin auth mi const router = express.Router(); router.get("/threads", getAllThreads); +router.get("/threads/:threadId", protect, getThreadById); //Forum Routes router.post("/", protect, addForum); //TODO Protect with admin auth From 37bd4b806d8c79de36dc17d194088e2751aec6fd Mon Sep 17 00:00:00 2001 From: Sean Date: Sun, 21 Apr 2024 10:22:27 -0400 Subject: [PATCH 26/30] CHE-65 Created CreateThread component and updated ThreadsDisplay to support functionality --- .../Forums/CreateThread/CreateThread.tsx | 96 +++++++++++++++++++ .../Forums/ThreadsDisplay/ThreadsDisplay.tsx | 20 +++- server/controllers/threadController.ts | 1 + 3 files changed, 116 insertions(+), 1 deletion(-) create mode 100644 client/src/components/Forums/CreateThread/CreateThread.tsx diff --git a/client/src/components/Forums/CreateThread/CreateThread.tsx b/client/src/components/Forums/CreateThread/CreateThread.tsx new file mode 100644 index 0000000..3d3efd2 --- /dev/null +++ b/client/src/components/Forums/CreateThread/CreateThread.tsx @@ -0,0 +1,96 @@ +import React, { useState, ChangeEvent, FormEvent } from "react"; +import axios from "axios"; + +interface CreateThreadProps { + forumId: string; + onClose: () => void; +} + +const CreateThread: React.FC = ({ forumId, onClose }) => { + const [formData, setFormData] = useState<{ title: string; content: string }>({ + title: "", + content: "", + }); + + const { title, content } = formData; + + const handleChange = ( + e: ChangeEvent + ) => { + setFormData({ ...formData, [e.target.name]: e.target.value }); + }; + + const handleSubmit = async (e: FormEvent) => { + e.preventDefault(); + try { + const response = await axios.post( + `/api/forums/${forumId}/threads`, + formData, + { + withCredentials: true, + } + ); + console.log("Thread created:", response.data); + onClose(); + } catch (error) { + console.error("Failed to create thread:", error); + //TODO userfeedback with errors + } + }; + + return ( +
      +
      +
      + + +
      +
      + +