diff --git a/server/controllers/forumController.ts b/server/controllers/forumController.ts new file mode 100644 index 0000000..0c9d161 --- /dev/null +++ b/server/controllers/forumController.ts @@ -0,0 +1,136 @@ +import Forum from "../models/forumModel"; +import Thread from "../models/threadModel"; +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" }, + }); + } +}; + +// 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" }, + }); + } +}; + +// 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" }, + }); + } +}; + +// 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" }, + }); + } +}; + +// 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/controllers/postController.ts b/server/controllers/postController.ts new file mode 100644 index 0000000..54084fc --- /dev/null +++ b/server/controllers/postController.ts @@ -0,0 +1,154 @@ +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 +// 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" }, + }); + } +}; + +// 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" }, + }); + } +}; + +// 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" }, + }); + } +}; + +// 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/controllers/threadController.ts b/server/controllers/threadController.ts new file mode 100644 index 0000000..cf8eb98 --- /dev/null +++ b/server/controllers/threadController.ts @@ -0,0 +1,186 @@ +import Post from "../models/postModel"; +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.id; + + 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" }, + }); + } +}; + +// 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" }, + }); + } +}; + +// 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" }, + }); + } +}; + +// 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" }, + }); + } +}; + +// 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/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/middleware/authMiddleware.ts b/server/middleware/authMiddleware.ts index e08abf3..b399c0e 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 = { id: user._id.toString() }; 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 new file mode 100644 index 0000000..6d9ab3b --- /dev/null +++ b/server/routes/forumRoutes.ts @@ -0,0 +1,49 @@ +import express from "express"; +import { + addForum, + deleteForum, + getAllForums, + getForumById, + updateForum, +} from "../controllers/forumController"; + +import { + createThread, + deleteThread, + getThreadById, + listThreadsByForumId, + updateThread, +} from "../controllers/threadController"; + +import { + listPostsByThreadId, + createPost, + updatePost, + deletePost, +} from "../controllers/postController"; + +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); +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; diff --git a/server/types/customRequest.ts b/server/types/customRequest.ts new file mode 100644 index 0000000..f3d8e34 --- /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; +}