From b1f51514769c20218e5f214490b29cb9cbf7dc3f Mon Sep 17 00:00:00 2001 From: Kathinka Sewell <4355167+kathinka@users.noreply.github.com> Date: Mon, 20 May 2024 16:28:00 +0200 Subject: [PATCH 01/41] Update backend dependencies and added some routes. added authentication and autorization made a basic setup for files in the backend --- backend/middleware/Middleware.js | 32 ++++++ backend/model/user-model.js | 38 ++++++++ backend/package.json | 3 + backend/routes/routes.js | 161 +++++++++++++++++++++++++++++++ backend/server.js | 23 ++--- package.json | 4 + 6 files changed, 250 insertions(+), 11 deletions(-) create mode 100644 backend/middleware/Middleware.js create mode 100644 backend/model/user-model.js create mode 100644 backend/routes/routes.js diff --git a/backend/middleware/Middleware.js b/backend/middleware/Middleware.js new file mode 100644 index 000000000..8b001768a --- /dev/null +++ b/backend/middleware/Middleware.js @@ -0,0 +1,32 @@ +import express, { response } from "express"; +import User from "../model/user-model"; + +const router = express.Router(); + +// add middleware to authenticate user +const authenticateUser = async (req, res, next) => { + const accessToken = req.header("Authorization"); + const user = await User.findOne({ accessToken }); + if (user) { + req.user = user; + next(); + } else { + res + .status(401) + .json({ loggedOut: true, message: "you must log in to gain access" }); + } +}; + +// add middleware to authorize user +const authorizeUser = (roles) => { + return (req, res, next) => { + if (!roles.includes(req.user.role)) { + return res + .status(403) + .json({ message: "You are not authorized to access this page" }); + } + next(); + }; +}; + +module.exports = { authenticateUser, authorizeUser }; diff --git a/backend/model/user-model.js b/backend/model/user-model.js new file mode 100644 index 000000000..78e00437a --- /dev/null +++ b/backend/model/user-model.js @@ -0,0 +1,38 @@ +import mongoose from "mongoose"; +import bcrypt from "bcrypt"; + +// Create a schema +const userSchema = new mongoose.Schema({ + name: { + type: String, + required: [true, "Name is required"], + unique: true, + }, + email: { + type: String, + required: [true, "Email is required"], + unique: true, + }, + password: { + type: String, + required: [true, "Password is required"], + minlength: [5, "Password must be at least 5 characters"], + }, + role: { + type: String, + enum: ["user", "writer", "editor", "admin"], + default: "user", + }, + accessToken: { + type: String, + default: () => { + return bcrypt.hashSync(Math.random().toString(36).substring(2), 10); + }, + }, +}); + +// Create a model +const User = mongoose.model("User", userSchema); + +// Export the model +export default User; diff --git a/backend/package.json b/backend/package.json index 8de5c4ce0..93c635797 100644 --- a/backend/package.json +++ b/backend/package.json @@ -14,6 +14,9 @@ "@babel/preset-env": "^7.16.11", "cors": "^2.8.5", "express": "^4.17.3", + "express-list-endpoints": "^7.1.0", + "express-list-routes": "^1.2.1", + "jsonwebtoken": "^9.0.2", "mongoose": "^8.0.0", "nodemon": "^3.0.1" } diff --git a/backend/routes/routes.js b/backend/routes/routes.js new file mode 100644 index 000000000..0cdfc6cff --- /dev/null +++ b/backend/routes/routes.js @@ -0,0 +1,161 @@ +import express, { response } from "express"; +import User from "../model/user-model"; +import listEndpoints from "express-list-endpoints"; +import bcrypt from "bcrypt"; +import { authorizeUser, authenticateUser } from "../middleware/Middleware"; + +const router = express.Router(); + +// add user +router.post("/users", async (req, res) => { + const { name, email, password } = req.body; + try { + const salt = bcrypt.genSaltSync(); + const newUser = await new User({ + name, + email, + password: bcrypt.hashSync(password, salt), + }).save(); + res.status(201).json({ + userId: newUser._id, + accessToken: newUser.accessToken, + }); + } catch (err) { + res + .status(400) + .json({ message: "Could not create user", errors: err.errors }); + } +}); +// get all users in the database +router.get("/users", async (req, res) => { + const users = await User.find(); + res.json(users); +}); + +//log in user +router.post("/sessions", async (req, res) => { + const { email, password } = req.body; + const user = await User.findOne({ email }); + if (user && bcrypt.compareSync(password, user.password)) { + res.json({ userId: user._id, accessToken: user.accessToken }); + } else { + res.status(400).json({ notFound: true, message: "User not found" }); + } +}); + +// Patch request to update user +router.patch("/users/:id", async (req, res) => { + const { id } = req.params; + const { name, email, password } = req.body; + const salt = bcrypt.genSaltSync(); + const updatedUser = await User.findByIdAndUpdate( + id, + { + name, + email, + password: bcrypt.hashSync(password, salt), + }, + { new: true } + ); + if (updatedUser) { + res.json(updatedUser); + } else { + res.status(404).json({ message: "User not found" }); + } +}); + +//route for getting content behind authentication - lets update this with something that makes sense later :) +router.get("/secrets", authenticateUser, (req, res) => { + res.send( + "The password is potato - you are authenticated and can see this members only content -lucky you!" + ); +}); + +// route for getting content behind authorization +router.get("/admin", authenticateUser, authorizeUser(["admin"]), (req, res) => { + res.send( + "This is the admin page - you are authorized to view this content - so much admin stuff to do here!" + ); +}); + +//admin update user +router.put( + "/admin/users/:id", + authenticateUser, + authorizeUser(["admin"]), + async (req, res) => { + const { id } = req.params; + const { name, email, role, password } = req.body; + const salt = bcrypt.genSaltSync(); + const updatedUser = await User.findByIdAndUpdate( + id, + { + name, + email, + role, + password: bcrypt.hashSync(password, salt), + }, + { new: true } + ); + if (updatedUser) { + res.json(updatedUser); + } else { + res.status(404).json({ message: "User not found" }); + } + } +); + +//endpoint for only updating the user role +router.patch("/admin/users/:id", async (req, res) => { + const { id } = req.params; + const { role } = req.body; + const updated = await updateUserRole(id, role); + if (updated) { + res.json(updated); + } else { + res.status(404).json({ message: "User not found" }); + } +}); + +//delete user +router.delete("/admin/users/:id", async (req, res) => { + const { id } = req.params; + const deletedUser = await User.findByIdAndDelete(id); + if (deletedUser) { + res.json({ message: "user deleted", deletedUser }); + } else { + res.status(404).json({ message: "User not found" }); + } +}); + +router.get("/", (req, res) => { + try { + const endpoints = listEndpoints(router); + const updatedEndpoints = endpoints.map((endpoint) => { + if (endpoint.path === "/") { + return { + path: endpoint.path, + methods: endpoint.methods, + queryParameters: [], + }; + } + return { + path: endpoint.path, + methods: endpoint.methods, + }; + }); + res.json(updatedEndpoints); + } catch (error) { + // If an error occurred, create a new error with a custom message + const customError = new Error( + "An error occurred while fetching the endpoints" + ); + res.status(404).json({ + success: false, + response: error, + message: customError.message, + }); + } +}); + +export default router; diff --git a/backend/server.js b/backend/server.js index dfe86fb8e..9d7c156db 100644 --- a/backend/server.js +++ b/backend/server.js @@ -1,25 +1,26 @@ import cors from "cors"; import express from "express"; import mongoose from "mongoose"; +import router from "./routes/routes.js"; -const mongoUrl = process.env.MONGO_URL || "mongodb://localhost/project-mongo"; +const mongoUrl = process.env.MONGO_URL || "mongodb://localhost/authAPI"; mongoose.connect(mongoUrl); mongoose.Promise = Promise; -// Defines the port the app will run on. Defaults to 8080, but can be overridden -// when starting the server. Example command to overwrite PORT env variable value: -// PORT=9000 npm start -const port = process.env.PORT || 8080; +const port = process.env.PORT || 8787; const app = express(); // Add middlewares to enable cors and json body parsing -app.use(cors()); +//allow all for now, restrict later +app.use( + cors({ + origin: ["*"], + methods: ["GET", "POST", "PUT", "DELETE"], + allowedHeaders: ["Content-Type", "Authorization"], + }) +); app.use(express.json()); - -// Start defining your routes here -app.get("/", (req, res) => { - res.send("Hello Technigo!"); -}); +app.use("/", router); // Start the server app.listen(port, () => { diff --git a/package.json b/package.json index d774b8cc3..ddc0008b0 100644 --- a/package.json +++ b/package.json @@ -3,5 +3,9 @@ "version": "1.0.0", "scripts": { "postinstall": "npm install --prefix backend" + }, + "dependencies": { + "bcrypt": "^5.1.1", + "bcryptjs": "^2.4.3" } } From 05fd5b19c526043f03ccc298e64e2fad8bc0c2f3 Mon Sep 17 00:00:00 2001 From: Kathinka Sewell <4355167+kathinka@users.noreply.github.com> Date: Mon, 20 May 2024 16:56:46 +0200 Subject: [PATCH 02/41] chore: Update backend dependencies and add bcrypt package --- backend/package.json | 1 + 1 file changed, 1 insertion(+) diff --git a/backend/package.json b/backend/package.json index 93c635797..6a942570a 100644 --- a/backend/package.json +++ b/backend/package.json @@ -12,6 +12,7 @@ "@babel/core": "^7.17.9", "@babel/node": "^7.16.8", "@babel/preset-env": "^7.16.11", + "bcrypt": "^5.1.1", "cors": "^2.8.5", "express": "^4.17.3", "express-list-endpoints": "^7.1.0", From 3afddf2d71b4fa76c518900b72e24a8f18de4677 Mon Sep 17 00:00:00 2001 From: Kathinka Sewell <4355167+kathinka@users.noreply.github.com> Date: Mon, 20 May 2024 19:00:06 +0200 Subject: [PATCH 03/41] Add admin routes for authentication and authorization --- backend/package.json | 1 + backend/routes/routes.js | 91 +++++++++++++++++++++++++++------------- backend/server.js | 2 + 3 files changed, 65 insertions(+), 29 deletions(-) diff --git a/backend/package.json b/backend/package.json index 6a942570a..0eee8c332 100644 --- a/backend/package.json +++ b/backend/package.json @@ -14,6 +14,7 @@ "@babel/preset-env": "^7.16.11", "bcrypt": "^5.1.1", "cors": "^2.8.5", + "dotenv": "^16.4.5", "express": "^4.17.3", "express-list-endpoints": "^7.1.0", "express-list-routes": "^1.2.1", diff --git a/backend/routes/routes.js b/backend/routes/routes.js index 0cdfc6cff..ffa95e8e6 100644 --- a/backend/routes/routes.js +++ b/backend/routes/routes.js @@ -5,6 +5,11 @@ import bcrypt from "bcrypt"; import { authorizeUser, authenticateUser } from "../middleware/Middleware"; const router = express.Router(); +const adminRouter = express.Router(); +adminRouter.use(authenticateUser, authorizeUser(["admin"])); + +// Add adminRouter to the main router +router.use("/admin", adminRouter); // add user router.post("/users", async (req, res) => { @@ -71,21 +76,23 @@ router.get("/secrets", authenticateUser, (req, res) => { ); }); -// route for getting content behind authorization -router.get("/admin", authenticateUser, authorizeUser(["admin"]), (req, res) => { +// route for getting content behind authorization find me at /admin/admin +adminRouter.get("/admin", (req, res) => { res.send( "This is the admin page - you are authorized to view this content - so much admin stuff to do here!" ); }); -//admin update user -router.put( - "/admin/users/:id", - authenticateUser, - authorizeUser(["admin"]), - async (req, res) => { +//admin update user - find me at /admin/admin/users/:id +adminRouter.put("/admin/users/:id", async (req, res) => { + try { const { id } = req.params; const { name, email, role, password } = req.body; + + if (!name || !email || !role || !password) { + return res.status(400).json({ error: "All fields are required" }); + } + const salt = bcrypt.genSaltSync(); const updatedUser = await User.findByIdAndUpdate( id, @@ -97,34 +104,60 @@ router.put( }, { new: true } ); + + if (!updatedUser) { + return res.status(404).json({ error: "User not found" }); + } + + res.json(updatedUser); + } catch (error) { + console.error(error); + res + .status(500) + .json({ error: "An error occurred while updating the user" }); + } +}); + +//endpoint for only updating the user role - find me at admin/admin/users/:id +adminRouter.patch("/admin/users/:id", async (req, res) => { + try { + const { id } = req.params; + const { role } = req.body; + const updatedUser = await User.findByIdAndUpdate( + id, + { role }, + { new: true } + ); if (updatedUser) { - res.json(updatedUser); + res.json({ + message: "User role updated", + success: true, + response: updatedUser, + }); } else { res.status(404).json({ message: "User not found" }); } - } -); - -//endpoint for only updating the user role -router.patch("/admin/users/:id", async (req, res) => { - const { id } = req.params; - const { role } = req.body; - const updated = await updateUserRole(id, role); - if (updated) { - res.json(updated); - } else { - res.status(404).json({ message: "User not found" }); + } catch (error) { + res + .status(500) + .json({ message: "An error occurred while updating the user" }); } }); -//delete user -router.delete("/admin/users/:id", async (req, res) => { - const { id } = req.params; - const deletedUser = await User.findByIdAndDelete(id); - if (deletedUser) { - res.json({ message: "user deleted", deletedUser }); - } else { - res.status(404).json({ message: "User not found" }); +//delete user - find me at /admin/admin/users/:id +adminRouter.delete("/admin/users/:id", async (req, res) => { + try { + const { id } = req.params; + const deletedUser = await User.findByIdAndDelete(id); + if (deletedUser) { + res.json({ message: "user deleted", deletedUser }); + } else { + res.status(404).json({ message: "User not found" }); + } + } catch (error) { + res + .status(500) + .json({ message: "An error occurred while deleting the user" }); } }); diff --git a/backend/server.js b/backend/server.js index 9d7c156db..a16e61130 100644 --- a/backend/server.js +++ b/backend/server.js @@ -2,6 +2,8 @@ import cors from "cors"; import express from "express"; import mongoose from "mongoose"; import router from "./routes/routes.js"; +import dotenv from "dotenv"; + const mongoUrl = process.env.MONGO_URL || "mongodb://localhost/authAPI"; mongoose.connect(mongoUrl); From 81ef732435a395d47a5714164e453c2c2ddcb39c Mon Sep 17 00:00:00 2001 From: Kathinka Sewell <4355167+kathinka@users.noreply.github.com> Date: Mon, 20 May 2024 20:47:25 +0200 Subject: [PATCH 04/41] Update user routes for signup and login --- backend/routes/routes.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/backend/routes/routes.js b/backend/routes/routes.js index ffa95e8e6..cc1709639 100644 --- a/backend/routes/routes.js +++ b/backend/routes/routes.js @@ -12,7 +12,7 @@ adminRouter.use(authenticateUser, authorizeUser(["admin"])); router.use("/admin", adminRouter); // add user -router.post("/users", async (req, res) => { +router.post("/signup", async (req, res) => { const { name, email, password } = req.body; try { const salt = bcrypt.genSaltSync(); @@ -38,7 +38,7 @@ router.get("/users", async (req, res) => { }); //log in user -router.post("/sessions", async (req, res) => { +router.post("/login", async (req, res) => { const { email, password } = req.body; const user = await User.findOne({ email }); if (user && bcrypt.compareSync(password, user.password)) { From 635c112fa97aacd3699cc31f52ee23f087159be8 Mon Sep 17 00:00:00 2001 From: Kathinka Sewell <4355167+kathinka@users.noreply.github.com> Date: Sat, 25 May 2024 01:15:11 +0200 Subject: [PATCH 05/41] chore: Update frontend and backend dependencies Update frontend and backend dependencies to the latest versions. Also, add necessary packages for authentication and authorization in the backend. Remove the deprecated useNewUrlParser and useUnifiedTopology options. Delete the package-lock.json files. Update the package.json files to remove vulnerabilities and add package-lock.json to the gitignore file. --- .env.example | 3 + backend/.env.example | 2 + backend/middleware/Middleware.js | 114 +++++++-- backend/model/blacklist-model.js | 14 ++ backend/model/user-model.js | 3 +- backend/package.json | 5 + backend/routes/adminRoutes.js | 149 ++++++++++++ backend/routes/routes.js | 242 ++++++++----------- backend/server.js | 52 +++- backend/utility/tokenBlacklist.js | 48 ++++ frontend/.env.example | 1 + frontend/components/Admin.jsx | 63 +++++ frontend/components/Dashboard.jsx | 52 ++++ frontend/components/Header.jsx | 0 frontend/components/Home.jsx | 27 +++ frontend/components/Login.jsx | 75 ++++++ frontend/components/Logout.jsx | 44 ++++ frontend/components/ProtectedRoutes.jsx | 49 ++++ frontend/components/Registration.jsx | 91 +++++++ frontend/components/forms/CreateUser.jsx | 84 +++++++ frontend/components/forms/DeleteUser.jsx | 33 +++ frontend/components/forms/UpdateUser.jsx | 80 ++++++ frontend/components/forms/UpdateUserRole.jsx | 62 +++++ frontend/package.json | 6 +- frontend/routes/AppRoutes.jsx | 27 +++ frontend/src/App.jsx | 9 +- 26 files changed, 1178 insertions(+), 157 deletions(-) create mode 100644 .env.example create mode 100644 backend/.env.example create mode 100644 backend/model/blacklist-model.js create mode 100644 backend/routes/adminRoutes.js create mode 100644 backend/utility/tokenBlacklist.js create mode 100644 frontend/.env.example create mode 100644 frontend/components/Admin.jsx create mode 100644 frontend/components/Dashboard.jsx create mode 100644 frontend/components/Header.jsx create mode 100644 frontend/components/Home.jsx create mode 100644 frontend/components/Login.jsx create mode 100644 frontend/components/Logout.jsx create mode 100644 frontend/components/ProtectedRoutes.jsx create mode 100644 frontend/components/Registration.jsx create mode 100644 frontend/components/forms/CreateUser.jsx create mode 100644 frontend/components/forms/DeleteUser.jsx create mode 100644 frontend/components/forms/UpdateUser.jsx create mode 100644 frontend/components/forms/UpdateUserRole.jsx create mode 100644 frontend/routes/AppRoutes.jsx diff --git a/.env.example b/.env.example new file mode 100644 index 000000000..b4128944e --- /dev/null +++ b/.env.example @@ -0,0 +1,3 @@ +VITE_API_KEY= +MONGO_URL= +SECRET= \ No newline at end of file diff --git a/backend/.env.example b/backend/.env.example new file mode 100644 index 000000000..9f7135d7d --- /dev/null +++ b/backend/.env.example @@ -0,0 +1,2 @@ +MONGO_URL= +SECRET= \ No newline at end of file diff --git a/backend/middleware/Middleware.js b/backend/middleware/Middleware.js index 8b001768a..5142c5f87 100644 --- a/backend/middleware/Middleware.js +++ b/backend/middleware/Middleware.js @@ -1,32 +1,114 @@ -import express, { response } from "express"; import User from "../model/user-model"; +import dotenv from "dotenv"; +import Blacklist from "../model/blacklist-model"; +import { tokenBlacklist } from "../utility/tokenBlacklist"; +import jwt from "jsonwebtoken"; -const router = express.Router(); +dotenv.config(); +const SECRET = process.env.SECRET || "toast is the best secret"; +//console.log("middleware SECRET: ", SECRET); -// add middleware to authenticate user const authenticateUser = async (req, res, next) => { - const accessToken = req.header("Authorization"); - const user = await User.findOne({ accessToken }); - if (user) { - req.user = user; - next(); + // Get the token from the request headers + const bearerHeader = req.headers["authorization"]; + //check if token is undefined, because if it is, we don't have any time for that - bzzzt! + if (typeof bearerHeader !== "undefined") { + //split the token like a lumberjack, and separate the bearer from the token + const bearer = bearerHeader.split(" "); + // Check if the bearer array has exactly 2 elements and the first element is 'Bearer' + if (bearer.length === 2 && bearer[0] === "Bearer") { + const bearerToken = bearer[1]; + try { + //decode the token 🤫 + const decoded = jwt.verify(bearerToken, SECRET); + // find user with the decoded id + const user = await User.findById(decoded.id); + //check if user exists and ready to party like a request object + if (user) { + req.user = user; + req.user.accessToken = bearerToken; // Set the access token for the user + //move to the next middleware -> authorizeUser or isLoggedIn to see if token is valid (not stored in blacklist) + next(); + } else { + res.status(401).json({ + loggedOut: true, + message: "you must log in to gain access", + }); + } + } catch (err) { + res.status(403).json({ loggedOut: true, message: "Invalid token" }); + } + } else { + res.status(403).json({ + loggedOut: true, + message: + "Invalid Authorization header format. It should be 'Bearer '", + }); + } } else { - res - .status(401) - .json({ loggedOut: true, message: "you must log in to gain access" }); + res.status(403).json({ loggedOut: true, message: "No token provided" }); } }; -// add middleware to authorize user +// authorize user const authorizeUser = (roles) => { return (req, res, next) => { + //check if user role is in the roles array and can hang out with the cool kids if (!roles.includes(req.user.role)) { - return res - .status(403) - .json({ message: "You are not authorized to access this page" }); + return res.status(403).json({ + message: `You are not authorized to access this page. Required roles: ${roles.join( + ", " + )}`, + }); } next(); }; }; -module.exports = { authenticateUser, authorizeUser }; +//log out user +const logoutUser = async (req, res, next) => { + try { + if (!req.user || !req.user.accessToken) { + return res.status(400).json({ message: "Invalid user or access token" }); + } + // Add the token to the blacklist, so it can't be used again + const token = req.user.accessToken; + const blacklistEntry = new Blacklist({ token: token }); + await blacklistEntry.save(); + //remove the access token from the user, this was a one time thing baby- you got to let it go + req.user.accessToken = null; + await req.user.save(); + //tell the user they are logged out, because we are polite like that, and dont ghost people + res.json({ message: "You are now logged out" }); + } catch (error) { + // if we can let that user go, we will let them know - but if we can't, we will let the console know + console.error("Error logging out user: ", error); + res.status(500).json({ + message: "An error occurred while logging out", + error: error.message, + }); + } +}; + +//check if user is logged in +const isLoggedIn = (req, res, next) => { + if (req.user) { + // Check if the user's token is in the blacklist, and if it is, tell them to get lost tho shall not pass + if (tokenBlacklist.includes(req.user.accessToken)) { + return ( + res + .status(401) + //tell the user they are not allowed to pass - like a bouncer at a club + .json({ message: "This token has been invalidated" }) + ); + } + next(); + } else { + //tell the user they are not logged in - those invalidated tokens are dead to us - begone ghost! + res.status(403).json({ message: "No user logged in" }); + } + // give success message if user is logged in - you are in the club, party on! 🎉 + res.json({ message: "You are logged in" }); +}; + +module.exports = { authenticateUser, authorizeUser, logoutUser, isLoggedIn }; diff --git a/backend/model/blacklist-model.js b/backend/model/blacklist-model.js new file mode 100644 index 000000000..566edb3c2 --- /dev/null +++ b/backend/model/blacklist-model.js @@ -0,0 +1,14 @@ +import mongoose from "mongoose"; + +const BlacklistSchema = new mongoose.Schema({ + token: { + type: String, + required: true, + unique: true, + }, +}); + +const Blacklist = mongoose.model("Blacklist", BlacklistSchema); + +// Export the model +export default Blacklist; diff --git a/backend/model/user-model.js b/backend/model/user-model.js index 78e00437a..f9526ff54 100644 --- a/backend/model/user-model.js +++ b/backend/model/user-model.js @@ -6,7 +6,8 @@ const userSchema = new mongoose.Schema({ name: { type: String, required: [true, "Name is required"], - unique: true, + minlength: [2, "Name must be at least 2 characters"], + maxlength: [30, "Name must be at most 30 characters"], }, email: { type: String, diff --git a/backend/package.json b/backend/package.json index 0eee8c332..454a96a92 100644 --- a/backend/package.json +++ b/backend/package.json @@ -12,12 +12,17 @@ "@babel/core": "^7.17.9", "@babel/node": "^7.16.8", "@babel/preset-env": "^7.16.11", + "atob": "^2.1.2", "bcrypt": "^5.1.1", + "cookie-parser": "^1.4.6", "cors": "^2.8.5", + "csurf": "^1.11.0", + "dompurify": "^3.1.4", "dotenv": "^16.4.5", "express": "^4.17.3", "express-list-endpoints": "^7.1.0", "express-list-routes": "^1.2.1", + "express-session": "^1.18.0", "jsonwebtoken": "^9.0.2", "mongoose": "^8.0.0", "nodemon": "^3.0.1" diff --git a/backend/routes/adminRoutes.js b/backend/routes/adminRoutes.js new file mode 100644 index 000000000..f9f9c7a64 --- /dev/null +++ b/backend/routes/adminRoutes.js @@ -0,0 +1,149 @@ +import express from "express"; +import User from "../model/user-model"; +import bcrypt from "bcrypt"; +import dotenv from "dotenv"; +import { authenticateUser, authorizeUser } from "../middleware/Middleware"; +import Blacklist from "../model/blacklist-model"; +import jwt from 'jsonwebtoken'; +dotenv.config() +const SECRET = process.env.SECRET || "toast is the best secret"; +//console.log("adminroutes SECRET: ", SECRET); + +const adminRouter = express.Router(); +adminRouter.use(authenticateUser, authorizeUser(["admin"])); + +// get all users in the database +adminRouter.get("/users", async (req, res) => { + try { + const users = await User.find(); + //users.map((user) => + // console.log(user.id)); + //console.log(users); + res.json(users); + } catch (error) { + res.status(500).json({ + message: "An error occurred while fetching users", + error: error.message, + }); + } +}); + +// route for getting content behind authorization find me at /admin +adminRouter.get("/", authenticateUser, authorizeUser(["admin"]), (req, res) => { + // This code will only run if the user is an admin + res.render("admin"); +}); + +//admin add user - find me at /admin/users +adminRouter.post("/users", async (req, res) => { + try { + const { name, email, role, password } = req.body; + + if (!name || !email || !role || !password) { + return res + .status(400) + .json({ error: "All fields are required", error: error.message }); + } + const salt = bcrypt.genSaltSync(); + const newUser = new User({ + name, + email, + role, + password: bcrypt.hashSync(password, salt), + }); + const savedUser = await newUser.save(); + res.json(savedUser); + } catch (error) { + res.status(500).json({ + error: "An error occurred while adding the user", + error: error.message, + }); + } +}); + +//admin update user - find me at /admin/users/:id +adminRouter.put("/users/:id", async (req, res) => { + try { + const { id } = req.params; + const { name, email, role, password } = req.body; + + if (!name || !email || !role || !password) { + return res + .status(400) + .json({ error: "All fields are required", error: error.message }); + } + + const salt = bcrypt.genSaltSync(); + const updatedUser = await User.findByIdAndUpdate( + id, + { + name, + email, + role, + password: bcrypt.hashSync(password, salt), + }, + { new: true } + ); + + if (!updatedUser) { + return res + .status(404) + .json({ error: "User not found", error: error.message }); + } + + res.json(updatedUser); + } catch (error) { + console.error(error); + res.status(500).json({ + error: "An error occurred while updating the user", + error: error.message, + }); + } +}); + +//endpoint for only updating the user role - find me at admin/users/:id +adminRouter.patch("/users/:id", async (req, res) => { + try { + const { id } = req.params; + const { role } = req.body; + const updatedUser = await User.findByIdAndUpdate( + id, + { role }, + { new: true } + ); + if (updatedUser) { + res.json({ + message: "User role updated", + success: true, + response: updatedUser, + }); + } else { + res.status(404).json({ message: "User not found", error: error.message }); + } + } catch (error) { + res.status(500).json({ + message: "An error occurred while updating the user", + error: error.message, + }); + } +}); + +//delete user - find me at /admin/users/:id +adminRouter.delete("/users/:id", async (req, res) => { + try { + const { id } = req.params; + const deletedUser = await User.findByIdAndDelete(id); + if (deletedUser) { + res.json({ message: "user deleted", deletedUser }); + } else { + res.status(404).json({ message: "User not found", error: error.message }); + } + } catch (error) { + res.status(500).json({ + message: "An error occurred while deleting the user", + error: error.message, + }); + } +}); + +export default adminRouter; diff --git a/backend/routes/routes.js b/backend/routes/routes.js index cc1709639..5cd3b6ef0 100644 --- a/backend/routes/routes.js +++ b/backend/routes/routes.js @@ -1,15 +1,43 @@ -import express, { response } from "express"; +import express from "express"; import User from "../model/user-model"; -import listEndpoints from "express-list-endpoints"; import bcrypt from "bcrypt"; -import { authorizeUser, authenticateUser } from "../middleware/Middleware"; +import { + authenticateUser, + logoutUser, + isLoggedIn, +} from "../middleware/Middleware"; +import jwt from 'jsonwebtoken'; +import dotenv from "dotenv"; + +dotenv.config() +const SECRET = process.env.SECRET ||"toast is the best secret"; +// console.log("routes SECRET: ", SECRET); const router = express.Router(); -const adminRouter = express.Router(); -adminRouter.use(authenticateUser, authorizeUser(["admin"])); -// Add adminRouter to the main router -router.use("/admin", adminRouter); +//check if user already exists +router.post("/exists", async (req, res) => { + const { email } = req.body; + try { + const user = await User.findOne({ email: email }); + if (user) { + res.status(400).json({ + exists: true, + message: "User already exists", + }); + } else { + res.status(200).json({ + exists: false, + message: "User does not exist", + }); + } + } catch (err) { + res.status(400).json({ + message: "An error occurred while checking if user exists", + error: error.message, + }); + } +}); // add user router.post("/signup", async (req, res) => { @@ -21,6 +49,14 @@ router.post("/signup", async (req, res) => { email, password: bcrypt.hashSync(password, salt), }).save(); + + // Generate a new access token + const newAccessToken = jwt.sign({ id: newUser._id }, SECRET, { + expiresIn: "1h", + }); + // Update the new user's access token + newUser.accessToken = newAccessToken; + res.status(201).json({ userId: newUser._id, accessToken: newUser.accessToken, @@ -31,23 +67,74 @@ router.post("/signup", async (req, res) => { .json({ message: "Could not create user", errors: err.errors }); } }); -// get all users in the database -router.get("/users", async (req, res) => { - const users = await User.find(); - res.json(users); -}); //log in user router.post("/login", async (req, res) => { - const { email, password } = req.body; - const user = await User.findOne({ email }); - if (user && bcrypt.compareSync(password, user.password)) { - res.json({ userId: user._id, accessToken: user.accessToken }); - } else { - res.status(400).json({ notFound: true, message: "User not found" }); + try { + const { email, password } = req.body; + const user = await User.findOne({ email }); + if (user && bcrypt.compareSync(password, user.password)) { + //generate the access token + const newAccessToken = jwt.sign({ id: user._id }, SECRET, { + expiresIn: "1h", + }); + // Send the access token back to the client + res.json({ + userId: user._id, + accessToken: newAccessToken, + role: user.role, + }); + // res.json({accessToken}); + } else { + res.status(400).json({ notFound: true, message: "User not found" }); + } + } catch (error) { + res.status(500).json({ + message: "An error occurred while logging in", + error: error.message, + }); + } +}); + +//route to verify if token is valid with middleware isLoggedIn +router.get("/verify", authenticateUser, isLoggedIn, (req, res) => { + res.json({ message: "You are logged in" }); +}); + +//route to get user role +router.get("/role", authenticateUser, (req, res) => { + res.json({ role: req.user.role }); + // console.log("role: ", req.user.role); +}); + + + +/* +// delete these later after testing +//add a test blacklist endpoint +router.get("/blacklist", async (req, res) => { + try { + const blacklist = await Blacklist.find(); + res.json(blacklist); + } catch (error) { + res.status(500).json({ message: "An error occurred while fetching the blacklist" , error: error.message }); } }); +router.post("/blacklist", async (req, res) => { + const { token } = req.body; + const newToken = await new Blacklist({ + token, + }).save(); +}); + +*/ +// Route to log out user +router.post("/logout", authenticateUser, logoutUser, (req, res) => { + console.log('Logging out user'); // Log a message when the route is hit + res.json({ message: "You are now logged out" }); +}); + // Patch request to update user router.patch("/users/:id", async (req, res) => { const { id } = req.params; @@ -65,130 +152,15 @@ router.patch("/users/:id", async (req, res) => { if (updatedUser) { res.json(updatedUser); } else { - res.status(404).json({ message: "User not found" }); + res.status(404).json({ message: "User not found", error: error.message }); } }); //route for getting content behind authentication - lets update this with something that makes sense later :) -router.get("/secrets", authenticateUser, (req, res) => { +router.get("/secrets", authenticateUser, isLoggedIn, (req, res) => { res.send( "The password is potato - you are authenticated and can see this members only content -lucky you!" ); }); -// route for getting content behind authorization find me at /admin/admin -adminRouter.get("/admin", (req, res) => { - res.send( - "This is the admin page - you are authorized to view this content - so much admin stuff to do here!" - ); -}); - -//admin update user - find me at /admin/admin/users/:id -adminRouter.put("/admin/users/:id", async (req, res) => { - try { - const { id } = req.params; - const { name, email, role, password } = req.body; - - if (!name || !email || !role || !password) { - return res.status(400).json({ error: "All fields are required" }); - } - - const salt = bcrypt.genSaltSync(); - const updatedUser = await User.findByIdAndUpdate( - id, - { - name, - email, - role, - password: bcrypt.hashSync(password, salt), - }, - { new: true } - ); - - if (!updatedUser) { - return res.status(404).json({ error: "User not found" }); - } - - res.json(updatedUser); - } catch (error) { - console.error(error); - res - .status(500) - .json({ error: "An error occurred while updating the user" }); - } -}); - -//endpoint for only updating the user role - find me at admin/admin/users/:id -adminRouter.patch("/admin/users/:id", async (req, res) => { - try { - const { id } = req.params; - const { role } = req.body; - const updatedUser = await User.findByIdAndUpdate( - id, - { role }, - { new: true } - ); - if (updatedUser) { - res.json({ - message: "User role updated", - success: true, - response: updatedUser, - }); - } else { - res.status(404).json({ message: "User not found" }); - } - } catch (error) { - res - .status(500) - .json({ message: "An error occurred while updating the user" }); - } -}); - -//delete user - find me at /admin/admin/users/:id -adminRouter.delete("/admin/users/:id", async (req, res) => { - try { - const { id } = req.params; - const deletedUser = await User.findByIdAndDelete(id); - if (deletedUser) { - res.json({ message: "user deleted", deletedUser }); - } else { - res.status(404).json({ message: "User not found" }); - } - } catch (error) { - res - .status(500) - .json({ message: "An error occurred while deleting the user" }); - } -}); - -router.get("/", (req, res) => { - try { - const endpoints = listEndpoints(router); - const updatedEndpoints = endpoints.map((endpoint) => { - if (endpoint.path === "/") { - return { - path: endpoint.path, - methods: endpoint.methods, - queryParameters: [], - }; - } - return { - path: endpoint.path, - methods: endpoint.methods, - }; - }); - res.json(updatedEndpoints); - } catch (error) { - // If an error occurred, create a new error with a custom message - const customError = new Error( - "An error occurred while fetching the endpoints" - ); - res.status(404).json({ - success: false, - response: error, - message: customError.message, - }); - } -}); - export default router; diff --git a/backend/server.js b/backend/server.js index a16e61130..6976b2620 100644 --- a/backend/server.js +++ b/backend/server.js @@ -2,13 +2,26 @@ import cors from "cors"; import express from "express"; import mongoose from "mongoose"; import router from "./routes/routes.js"; +import adminRouter from "./routes/adminRoutes.js"; import dotenv from "dotenv"; - +import listEndpoints from "express-list-endpoints"; +dotenv.config(); const mongoUrl = process.env.MONGO_URL || "mongodb://localhost/authAPI"; mongoose.connect(mongoUrl); mongoose.Promise = Promise; +const connection = mongoose.connection; + +// Event listeners for the connection for debug +connection.on("connected", () => { + // console.log("Mongoose successfully connected to " + mongoUrl); +}); +// Event listeners for the connection for debug +connection.on("error", (error) => { + // console.log("Mongoose connection error: " + error); +}); + const port = process.env.PORT || 8787; const app = express(); @@ -16,13 +29,46 @@ const app = express(); //allow all for now, restrict later app.use( cors({ - origin: ["*"], - methods: ["GET", "POST", "PUT", "DELETE"], + origin: ["*", "http://localhost:5173"], + credentials: true, + methods: ["GET", "POST", "PATCH", "PUT", "DELETE"], allowedHeaders: ["Content-Type", "Authorization"], }) ); + app.use(express.json()); app.use("/", router); +app.use("/admin", adminRouter); + +app.get("/", (req, res) => { + try { + const endpoints = listEndpoints(router); + const updatedEndpoints = endpoints.map((endpoint) => { + if ((endpoint.path === "/") || (endpoint.path === "/admin") ){ + return { + path: endpoint.path, + methods: endpoint.methods, + queryParameters: [], + }; + } + return { + path: endpoint.path, + methods: endpoint.methods, + }; + }); + res.json(updatedEndpoints); + } catch (error) { + // If an error occurred, create a new error with a custom message + const customError = new Error( + "An error occurred while fetching the endpoints" + ); + res.status(404).json({ + success: false, + response: error, + message: customError.message, + }); + } +}); // Start the server app.listen(port, () => { diff --git a/backend/utility/tokenBlacklist.js b/backend/utility/tokenBlacklist.js new file mode 100644 index 000000000..db0b9f86c --- /dev/null +++ b/backend/utility/tokenBlacklist.js @@ -0,0 +1,48 @@ +import atob from "atob"; // Import the atob function to decode base64 strings +import Blacklist from "../model/blacklist-model.js"; // Import the Blacklist model + +// Remove expired tokens from the blacklist to avoid memory leaks, and schedule the next cleanup in an hour +//clutter free db please. my server is not a hoarder +const cleanupBlacklist = () => { + const now = Date.now() / 1000; // Get the current time in seconds + + // Find all tokens in the blacklist + Blacklist.find() + .then((tokens) => { + // Filter out tokens that have expired + const validTokens = tokens.filter((token) => { + let payload; + try { + const parts = token.token.split("."); + if (parts.length !== 3) { + throw new Error("Invalid token"); + } + payload = JSON.parse(atob(parts[1])); + } catch (error) { + console.error("Error parsing token payload: ", error); + return false; // Skip this token if there's an error + } + + return payload.exp > now; + }); + + // Remove expired tokens from the blacklist - wish it was this easy in real life + const expiredTokens = tokens.filter( + (token) => !validTokens.includes(token) + ); + expiredTokens.forEach((token) => { + Blacklist.deleteOne({ _id: token._id }).catch((error) => + console.error(error) + ); + }); + }) + .catch((error) => console.error(error)); + + // Schedule the next cleanup - Cinderella, you shall go to the ball but be back in an hour + setTimeout(cleanupBlacklist, 60 * 60 * 1000); // Clean up every hour +}; + +// Start the cleanup schedule - Cinderella, mop the floor and clean the windows of dirty tokens +cleanupBlacklist(); + +export { cleanupBlacklist }; diff --git a/frontend/.env.example b/frontend/.env.example new file mode 100644 index 000000000..c340407de --- /dev/null +++ b/frontend/.env.example @@ -0,0 +1 @@ +VITE_API_KEY= \ No newline at end of file diff --git a/frontend/components/Admin.jsx b/frontend/components/Admin.jsx new file mode 100644 index 000000000..1bc62a5a9 --- /dev/null +++ b/frontend/components/Admin.jsx @@ -0,0 +1,63 @@ +import { useState, useEffect, useCallback } from 'react'; + +import { Logout } from './Logout'; +import { UpdateUser } from './forms/UpdateUser'; +import { DeleteUser } from './forms/DeleteUser'; +import { CreateUser } from './forms/CreateUser'; +import { UpdateUserRole } from './forms/UpdateUserRole'; + + +export const Admin = () => { + const apiKey = import.meta.env.VITE_API_KEY; + const API = apiKey + "/admin"; + const [users, setUsers] = useState([]); + const token = sessionStorage.getItem('token'); + + const getUsers = useCallback(async () => { + try { + const response = await fetch(`${API}/users`, { + method: 'GET', + headers: { + 'Content-Type': 'application/json', + 'Authorization': `Bearer ${token}` + } + }); + const data = await response.json(); + setUsers(data); + } catch (error) { + console.error('Failed to fetch users:', error); + } + }, [API, token, setUsers]); + + useEffect(() => { + getUsers(); + }, [getUsers]); + + return ( +
+ +

Admin page

+

Wow - so much admin stuff

+

As an admin can you do lots of exclusive stuff: create and delete users, update user roles, and userinfo

+ + + + + + + + + + {users.map(({ _id, name, email, role }) => ( +
+
    +
  • ID: {_id}
  • +
  • NAME: {name}
  • +
  • EMAIL:{email}
  • +
  • ROLE: {role}
  • +
+
+ ))} +
+ ); +} diff --git a/frontend/components/Dashboard.jsx b/frontend/components/Dashboard.jsx new file mode 100644 index 000000000..da14e44ec --- /dev/null +++ b/frontend/components/Dashboard.jsx @@ -0,0 +1,52 @@ +import { useNavigate } from "react-router-dom" +import { Logout } from "./Logout" +import { useState, useEffect } from "react"; + +export const Dashboard = () => { + const [isAdmin, setIsAdmin] = useState(false); + const navigate = useNavigate(); + const apiKey = import.meta.env.VITE_API_KEY; + const API = apiKey + "/role"; + const token = sessionStorage.getItem('token'); + + useEffect(() => { + const fetchData = async () => { + await fetch(`${API}`, { + method: 'GET', + headers: { + 'Content-Type': 'application/json', + 'Authorization': `Bearer ${token}` + }, + }) + .then((res) => { + if (!res.ok) { + throw new Error(`Server error: ${res.status}`); + } + return res.json(); + }) + .then((json) => { + setIsAdmin(json.role === 'admin'); + }) + .catch((err) => { + console.error(err); + }); + }; + + fetchData(); + } + , [token, API]); + + return ( + <> +

Dashboard

+

This is a page you have to be logged into see!

+ + + {isAdmin ? ( + + ) : ( + + )} + + ); +} \ No newline at end of file diff --git a/frontend/components/Header.jsx b/frontend/components/Header.jsx new file mode 100644 index 000000000..e69de29bb diff --git a/frontend/components/Home.jsx b/frontend/components/Home.jsx new file mode 100644 index 000000000..afec235fb --- /dev/null +++ b/frontend/components/Home.jsx @@ -0,0 +1,27 @@ +import { Login } from "./Login.jsx" +import { Registration } from "./Registration.jsx" +import {useState} from 'react'; + + +export const Home = () => { + const [showLogin, setShowLogin] = useState(true); + return( + <> + +

Home

+ {showLogin ? ( + <> +

Not a member? Register here

+ + + + ) : ( + <> +

Already a member? Login here

+ + + + )} + + ); +} diff --git a/frontend/components/Login.jsx b/frontend/components/Login.jsx new file mode 100644 index 000000000..79466a98d --- /dev/null +++ b/frontend/components/Login.jsx @@ -0,0 +1,75 @@ +import { useState } from "react"; +import { Logout } from "./Logout.jsx"; +import { useNavigate } from "react-router-dom"; + +export const Login = () => { + const [email, setEmail] = useState(""); + const [password, setPassword] = useState(""); + const [message, setMessage] = useState(""); + const navigate = useNavigate(); + const apiKey = import.meta.env.VITE_API_KEY; + const API = apiKey + "/login"; + + + const handleLogin = (event) => { + event.preventDefault(); + fetch(API, { + method: "POST", + body: JSON.stringify({ email, password }), + headers: { "Content-Type": "application/json" }, + + }) + .then((res) => { + if (!res.ok) { + throw new Error("Unable to log in. Please try again."); + } + return res.json(); + }) + .then((json) => { + setMessage("Login successful!"); + navigate("/"); + // Store the token in session storage + sessionStorage.setItem('token', json.accessToken); + + + + + }) + .catch((err) => { + setMessage(err.message); + }); + }; + + return ( + +
+

Login

+
+ + + + +
+ {message && ( +
+

{message}

+ +
+ )} +
+ ) +}; \ No newline at end of file diff --git a/frontend/components/Logout.jsx b/frontend/components/Logout.jsx new file mode 100644 index 000000000..1c55632c5 --- /dev/null +++ b/frontend/components/Logout.jsx @@ -0,0 +1,44 @@ + +import { useNavigate } from "react-router-dom"; +import { useState } from "react"; + +export const Logout = () => { + const [message, setMessage] = useState(""); + const apiKey = import.meta.env.VITE_API_KEY; + const API = apiKey + "/logout"; + const navigate = useNavigate(); + + const handleLogout = async () => { + try { + // Retrieve the token from session storage + const yourToken = sessionStorage.getItem('token'); + const response = await fetch(API, { + method: "POST", + headers: { + "Content-Type": "application/json", + "Authorization": `Bearer ${yourToken}` + }, + }); + const data = await response.json(); + console.log(data); + // Let's Kon-Mari that token who does not spark joy anymore. it has no purpose and is no longer worthy of our neat session storage + sessionStorage.removeItem('token'); + setMessage("Logout successful! Please sign in."); + navigate("/"); + + + } catch (error) { + console.log(error); + } + }; + + return ( +
+

Logout

+

{message}

+ +
+ ); +} + + diff --git a/frontend/components/ProtectedRoutes.jsx b/frontend/components/ProtectedRoutes.jsx new file mode 100644 index 000000000..53957affb --- /dev/null +++ b/frontend/components/ProtectedRoutes.jsx @@ -0,0 +1,49 @@ +import { useNavigate } from 'react-router-dom'; +import PropTypes from 'prop-types'; +import { useEffect, useState } from 'react'; + +// ProtectedRoute component handles the verification of the token for all protected routes +export const ProtectedRoute = ({ children }) => { + const navigate = useNavigate(); + const [isLoading, setIsLoading] = useState(true); + const token = sessionStorage.getItem('token'); + + useEffect(() => { + const verifyToken = async () => { + try { + const response = await fetch('/verify', { + method: 'GET', + headers: { + 'Content-Type': 'application/json', + "Authorization": `Bearer ${token}`, + }, + }); + + if (!response.ok) { + throw new Error('Token verification failed'); + } + + setIsLoading(false); + } catch (error) { + sessionStorage.removeItem('token'); + navigate('/home'); + } + }; + + if (token) { + verifyToken(); + } else { + navigate('/home'); + } + }, [navigate, token]); + + if (isLoading || !token) { + return null; + } + + return children; +}; + +ProtectedRoute.propTypes = { + children: PropTypes.node.isRequired, +}; \ No newline at end of file diff --git a/frontend/components/Registration.jsx b/frontend/components/Registration.jsx new file mode 100644 index 000000000..7ac9cabc1 --- /dev/null +++ b/frontend/components/Registration.jsx @@ -0,0 +1,91 @@ +import { useState } from "react"; +import { useNavigate } from "react-router-dom"; + +export const Registration = () => { + const [name, setName] = useState(""); + const [email, setEmail] = useState(""); + const [password, setPassword] = useState(""); + const [message, setMessage] = useState(""); + const navigate = useNavigate(); + const apiKey = import.meta.env.VITE_API_KEY; + const signup = apiKey + "/signup"; + const exist = apiKey + "/exists"; + + const handleRegistration = (event) => { + event.preventDefault(); + + // First check if the user already exists because brainfog is a post-covid vibe, and people have to many accounts and things going on in their lives to remember everything + fetch(exist, { + method: "POST", + body: JSON.stringify({ email }), + headers: { "Content-Type": "application/json" }, + }) + .then((res) => res.json()) + .then((data) => { + if (data.exists) { + // If the user exists, we alert them, and redirect them to the login page because we are super helpful but not patronizing + alert("Woops! You already have an account you silly goose 🪿, Let's redirect you to our loginpage."); + navigate("/login"); + } else { + // If the user does not exist silent celebrate our new member🥳 then play it cool and nonchalant, and proceed with the registration + fetch(signup, { + method: "POST", + body: JSON.stringify({ name, email, password }), + headers: { "Content-Type": "application/json" }, + }) + .then((res) => { + if (!res.ok) { + throw new Error("Unable to register. Please try again."); + } + return res.json(); + }) + .then((json) => { + console.log(json); + setMessage("Registration successful! Please sign in."); + navigate("/login"); + }) + .catch((err) => { + setMessage(err.message); + }); + } + }) + .catch(() => { + setMessage("An error occurred while checking if user exists"); + }); + }; + + return ( +
+

Register

+
+ + + + +
+ {message &&

{message}

} + +
+ ); +} diff --git a/frontend/components/forms/CreateUser.jsx b/frontend/components/forms/CreateUser.jsx new file mode 100644 index 000000000..4513e377c --- /dev/null +++ b/frontend/components/forms/CreateUser.jsx @@ -0,0 +1,84 @@ +import { useState } from 'react'; + +const apiKey = import.meta.env.VITE_API_KEY; +const API = apiKey + "/admin" + + +export const CreateUser = () => { + const [name, setName] = useState(''); + const [email, setEmail] = useState(''); + const [role, setRole] = useState('user'); + const [password, setPassword] = useState(''); + const [errors, setErrors] = useState({}); + const [serverError, setServerError] = useState(''); + const Update = async () => { + const token = sessionStorage.getItem('token'); + try { + const response = await fetch(`${API}/users`, { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + 'Authorization': `Bearer ${token}` + }, + body: JSON.stringify({ name, email, role, password }), + }); + if (!response.ok) { + const errorData = await response.json(); + throw new Error(errorData.error); + } + const data = await response.json(); + console.log(data); + } catch (error) { + console.error(error); + if (error.message.includes('E11000')) { + setServerError('A user with the same email already exists.'); + } else { + setServerError('An unexpected error occurred.'); + } + } + }; + + const handleSubmit = (e) => { + e.preventDefault(); + let errors = {}; + + if (!name) errors.name = "Name is required."; + if (!email) errors.email = "Email is required."; + if (!password) errors.password = "Password is required."; + if (!role) errors.role = "Role is required."; + setErrors(errors) + + if (Object.keys(errors).length === 0) { + // If no errors, celebrate and send fresh user data to the server + Update(); + } + }; + + + return ( +
+ + + + setName(e.target.value.trim())} /> + {errors.name &&

{errors.name}

} + + setEmail(e.target.value.trim())} /> + {errors.email &&

{errors.email}

} + + + + {errors.role &&

{errors.role}

} + + setPassword(e.target.value.trim())} /> + {errors.password &&

{errors.password}

} + + {serverError &&

{serverError}

} +
+ ); +} \ No newline at end of file diff --git a/frontend/components/forms/DeleteUser.jsx b/frontend/components/forms/DeleteUser.jsx new file mode 100644 index 000000000..c0dbb61ca --- /dev/null +++ b/frontend/components/forms/DeleteUser.jsx @@ -0,0 +1,33 @@ + +export const DeleteUser = () => { + const apiKey = import.meta.env.VITE_API_KEY; + const API = apiKey + "/admin"; + const token = sessionStorage.getItem('token'); + + const Delete = async (e) => { + e.preventDefault(); + const id = e.target.id.value; + try { + const response = await fetch(`${API}/users/${id}`, { + method: 'DELETE', + headers: { + 'Content-Type': 'application/json', + 'Authorization': `Bearer ${token}` + }, + }); + const data = await response.json(); + console.log(data); + } + catch (error) { + console.error(error); + } + } + return ( +
+ + + + +
+ ); +}; diff --git a/frontend/components/forms/UpdateUser.jsx b/frontend/components/forms/UpdateUser.jsx new file mode 100644 index 000000000..52ff0b666 --- /dev/null +++ b/frontend/components/forms/UpdateUser.jsx @@ -0,0 +1,80 @@ +import { useState } from 'react'; + +const apiKey = import.meta.env.VITE_API_KEY; +const API = apiKey + "/admin" +const token = sessionStorage.getItem('token'); + +export const UpdateUser = () => { + const [id, setId] = useState(''); + const [name, setName] = useState(''); + const [email, setEmail] = useState(''); + const [role, setRole] = useState('user'); + const [password, setPassword] = useState(''); +const [errors, setErrors] = useState({}); + const Update = async () => { + console.log('Token:', token); // Log the token + try { + const response = await fetch(`${API}/users/${id}`, { + method: 'PUT', + headers: { + 'Content-Type': 'application/json', + 'Authorization': `Bearer ${token}` + }, + body: JSON.stringify({ name, email, role, password }), + }); + if (!response.ok) { + console.log('Response status:', response.status); // Log the response status + } + const data = await response.json(); + console.log(data); + } catch (error) { + console.error(error); + } + }; + + const handleSubmit = (e) => { + e.preventDefault(); + let errors = {}; + + if (!id) errors.id = "ID is required."; + if (!name) errors.name = "Name is required."; + if (!email) errors.email = "Email is required."; + if (!password) errors.password = "Password is required."; + if (!role) errors.role = "Role is required."; + setErrors(errors) + + if (Object.keys(errors).length === 0) { + // If no errors, celebrate and send the data to the server + Update(); + } + }; + + + return ( +
+ + + setId(e.target.value.trim())} /> + {errors.id &&

{errors.id}

} + + setName(e.target.value.trim())} /> + {errors.name &&

{errors.name}

} + + setEmail(e.target.value.trim())} /> + {errors.email &&

{errors.email}

} + + + + {errors.role &&

{errors.role}

} + + setPassword(e.target.value.trim())} /> + {errors.password &&

{errors.password}

} + +
+ ); +} \ No newline at end of file diff --git a/frontend/components/forms/UpdateUserRole.jsx b/frontend/components/forms/UpdateUserRole.jsx new file mode 100644 index 000000000..44bcf4963 --- /dev/null +++ b/frontend/components/forms/UpdateUserRole.jsx @@ -0,0 +1,62 @@ +import { useState } from 'react'; + +export const UpdateUserRole = () => { + const [id, setId] = useState(''); + const [role, setRole] = useState('user'); + const [error, setError] = useState(''); + const [success, setSuccess] = useState(''); + const apiKey = import.meta.env.VITE_API_KEY; + const API = apiKey + "/admin"; + console.log(API); + const token = sessionStorage.getItem('token'); + + const updateRole = async (e) => { + e.preventDefault(); + const id = e.target.id.value; + const role = e.target.role.value; + + if (!id || !role) { + setError('Please fill in all fields'); + return; + } + + try { + const response = await fetch(`${API}/users/${id}`, { + method: 'PATCH', + headers: { + 'Content-Type': 'application/json', + 'Authorization': `Bearer ${token}` + }, + body: JSON.stringify({ role }), + }); + + if (!response.ok) { + throw new Error('Network response was not ok'); + } + + const data = await response.json(); + console.log(data); + setSuccess('User role updated successfully'); + } catch (error) { + console.error(error); + setError('An error occurred while updating the user role'); + } + }; + return ( +
+ {error &&

{error}

} + {success &&

{success}

} + + + setId(e.target.value.trim())} /> + + + +
+ ); +}; \ No newline at end of file diff --git a/frontend/package.json b/frontend/package.json index e9c95b79f..9debadaac 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -10,8 +10,12 @@ "preview": "vite preview" }, "dependencies": { + "dompurify": "^3.1.4", + "dotenv": "^16.4.5", + "jwt-decode": "^3.1.2", "react": "^18.2.0", - "react-dom": "^18.2.0" + "react-dom": "^18.2.0", + "react-router-dom": "^6.23.1" }, "devDependencies": { "@types/react": "^18.2.15", diff --git a/frontend/routes/AppRoutes.jsx b/frontend/routes/AppRoutes.jsx new file mode 100644 index 000000000..8d78b5f85 --- /dev/null +++ b/frontend/routes/AppRoutes.jsx @@ -0,0 +1,27 @@ +import { BrowserRouter as Router, Routes, Route } from 'react-router-dom'; +import { Registration } from "../components/Registration.jsx"; +import { Login } from "../components/Login.jsx"; +import { Home } from "../components/Home.jsx"; +import { Dashboard } from "../components/Dashboard.jsx"; +import { ProtectedRoute } from '../components/ProtectedRoutes.jsx'; +import { Admin } from '../components/Admin.jsx'; + +export const AppRoutes = () => { + + + return ( + + + + } fallbackComponent={} /> + } fallbackComponent={} /> + } /> + } /> + } /> + } /> + + + + + ) +}; diff --git a/frontend/src/App.jsx b/frontend/src/App.jsx index 1091d4310..6eb2b2659 100644 --- a/frontend/src/App.jsx +++ b/frontend/src/App.jsx @@ -1,3 +1,10 @@ +import { AppRoutes } from '/routes/AppRoutes.jsx'; + export const App = () => { - return
Find me in src/app.jsx!
; + return ( + + + + + ); }; From 9e7bc3e9b55781c8feb829838f46a28dada48901 Mon Sep 17 00:00:00 2001 From: Kathinka Sewell <4355167+kathinka@users.noreply.github.com> Date: Sun, 26 May 2024 22:41:30 +0200 Subject: [PATCH 06/41] Remove the netlify.toml file that specified the build configuration for Netlify. This file is no longer needed as the build configuration has been updated in the recent user commits. --- netlify.toml | 6 ------ 1 file changed, 6 deletions(-) delete mode 100644 netlify.toml diff --git a/netlify.toml b/netlify.toml deleted file mode 100644 index 95443a1f3..000000000 --- a/netlify.toml +++ /dev/null @@ -1,6 +0,0 @@ -# This file tells netlify where the code for this project is and -# how it should build the JavaScript assets to deploy from. -[build] - base = "frontend/" - publish = "build/" - command = "npm run build" From 801a64761854eb68b84d8c17dc165f8657451701 Mon Sep 17 00:00:00 2001 From: Kathinka Sewell <4355167+kathinka@users.noreply.github.com> Date: Sun, 26 May 2024 22:46:52 +0200 Subject: [PATCH 07/41] Update frontend and backend dependencies, remove deprecated options, and optimize package management --- frontend/components/Admin.jsx | 1 - frontend/components/Dashboard.jsx | 2 +- frontend/components/Header.jsx | 0 frontend/components/Home.jsx | 17 ++++++++--------- frontend/components/Login.jsx | 8 ++------ frontend/components/Logout.jsx | 1 - frontend/components/forms/CreateUser.jsx | 1 - frontend/components/forms/UpdateUser.jsx | 2 +- 8 files changed, 12 insertions(+), 20 deletions(-) delete mode 100644 frontend/components/Header.jsx diff --git a/frontend/components/Admin.jsx b/frontend/components/Admin.jsx index 1bc62a5a9..51907b352 100644 --- a/frontend/components/Admin.jsx +++ b/frontend/components/Admin.jsx @@ -6,7 +6,6 @@ import { DeleteUser } from './forms/DeleteUser'; import { CreateUser } from './forms/CreateUser'; import { UpdateUserRole } from './forms/UpdateUserRole'; - export const Admin = () => { const apiKey = import.meta.env.VITE_API_KEY; const API = apiKey + "/admin"; diff --git a/frontend/components/Dashboard.jsx b/frontend/components/Dashboard.jsx index da14e44ec..2de40eeaa 100644 --- a/frontend/components/Dashboard.jsx +++ b/frontend/components/Dashboard.jsx @@ -40,7 +40,7 @@ export const Dashboard = () => { <>

Dashboard

This is a page you have to be logged into see!

- + {isAdmin ? ( diff --git a/frontend/components/Header.jsx b/frontend/components/Header.jsx deleted file mode 100644 index e69de29bb..000000000 diff --git a/frontend/components/Home.jsx b/frontend/components/Home.jsx index afec235fb..896d78453 100644 --- a/frontend/components/Home.jsx +++ b/frontend/components/Home.jsx @@ -1,15 +1,14 @@ import { Login } from "./Login.jsx" import { Registration } from "./Registration.jsx" -import {useState} from 'react'; - +import { useState } from 'react'; export const Home = () => { const [showLogin, setShowLogin] = useState(true); - return( - <> + return ( + <> -

Home

- {showLogin ? ( +

Home

+ {showLogin ? ( <>

Not a member? Register here

@@ -20,8 +19,8 @@ export const Home = () => {

Already a member? Login here

- + )} - - ); + + ); } diff --git a/frontend/components/Login.jsx b/frontend/components/Login.jsx index 79466a98d..abb5ebed9 100644 --- a/frontend/components/Login.jsx +++ b/frontend/components/Login.jsx @@ -29,11 +29,7 @@ export const Login = () => { setMessage("Login successful!"); navigate("/"); // Store the token in session storage - sessionStorage.setItem('token', json.accessToken); - - - - + sessionStorage.setItem('token', json.accessToken); }) .catch((err) => { setMessage(err.message); @@ -67,7 +63,7 @@ export const Login = () => { {message && (

{message}

- +
)} diff --git a/frontend/components/Logout.jsx b/frontend/components/Logout.jsx index 1c55632c5..1ddda867b 100644 --- a/frontend/components/Logout.jsx +++ b/frontend/components/Logout.jsx @@ -26,7 +26,6 @@ export const Logout = () => { setMessage("Logout successful! Please sign in."); navigate("/"); - } catch (error) { console.log(error); } diff --git a/frontend/components/forms/CreateUser.jsx b/frontend/components/forms/CreateUser.jsx index 4513e377c..54c3749ed 100644 --- a/frontend/components/forms/CreateUser.jsx +++ b/frontend/components/forms/CreateUser.jsx @@ -3,7 +3,6 @@ import { useState } from 'react'; const apiKey = import.meta.env.VITE_API_KEY; const API = apiKey + "/admin" - export const CreateUser = () => { const [name, setName] = useState(''); const [email, setEmail] = useState(''); diff --git a/frontend/components/forms/UpdateUser.jsx b/frontend/components/forms/UpdateUser.jsx index 52ff0b666..ef4330b2d 100644 --- a/frontend/components/forms/UpdateUser.jsx +++ b/frontend/components/forms/UpdateUser.jsx @@ -10,7 +10,7 @@ export const UpdateUser = () => { const [email, setEmail] = useState(''); const [role, setRole] = useState('user'); const [password, setPassword] = useState(''); -const [errors, setErrors] = useState({}); + const [errors, setErrors] = useState({}); const Update = async () => { console.log('Token:', token); // Log the token try { From 37f88891f0519e493518a4cc83f0f2bba53a67e4 Mon Sep 17 00:00:00 2001 From: Kathinka Sewell <4355167+kathinka@users.noreply.github.com> Date: Sun, 26 May 2024 23:18:15 +0200 Subject: [PATCH 08/41] cleanup --- backend/package.json | 2 +- backend/routes/adminRoutes.js | 10 --------- backend/routes/routes.js | 41 ++++------------------------------- backend/server.js | 16 ++------------ 4 files changed, 7 insertions(+), 62 deletions(-) diff --git a/backend/package.json b/backend/package.json index 454a96a92..2e98b3eef 100644 --- a/backend/package.json +++ b/backend/package.json @@ -1,7 +1,7 @@ { "name": "project-auth-backend", "version": "1.0.0", - "description": "Starter project to get up and running with express quickly", + "description": "Project Aunt Authy - a test project for authentication - authentication with JWT, bcrypt, express, mongoose, and one strict Aunt Authy", "scripts": { "start": "babel-node server.js", "dev": "nodemon server.js --exec babel-node" diff --git a/backend/routes/adminRoutes.js b/backend/routes/adminRoutes.js index f9f9c7a64..71cd3a0c8 100644 --- a/backend/routes/adminRoutes.js +++ b/backend/routes/adminRoutes.js @@ -1,13 +1,7 @@ import express from "express"; import User from "../model/user-model"; import bcrypt from "bcrypt"; -import dotenv from "dotenv"; import { authenticateUser, authorizeUser } from "../middleware/Middleware"; -import Blacklist from "../model/blacklist-model"; -import jwt from 'jsonwebtoken'; -dotenv.config() -const SECRET = process.env.SECRET || "toast is the best secret"; -//console.log("adminroutes SECRET: ", SECRET); const adminRouter = express.Router(); adminRouter.use(authenticateUser, authorizeUser(["admin"])); @@ -16,9 +10,6 @@ adminRouter.use(authenticateUser, authorizeUser(["admin"])); adminRouter.get("/users", async (req, res) => { try { const users = await User.find(); - //users.map((user) => - // console.log(user.id)); - //console.log(users); res.json(users); } catch (error) { res.status(500).json({ @@ -38,7 +29,6 @@ adminRouter.get("/", authenticateUser, authorizeUser(["admin"]), (req, res) => { adminRouter.post("/users", async (req, res) => { try { const { name, email, role, password } = req.body; - if (!name || !email || !role || !password) { return res .status(400) diff --git a/backend/routes/routes.js b/backend/routes/routes.js index 5cd3b6ef0..7a754365c 100644 --- a/backend/routes/routes.js +++ b/backend/routes/routes.js @@ -6,13 +6,11 @@ import { logoutUser, isLoggedIn, } from "../middleware/Middleware"; -import jwt from 'jsonwebtoken'; +import jwt from "jsonwebtoken"; import dotenv from "dotenv"; -dotenv.config() -const SECRET = process.env.SECRET ||"toast is the best secret"; -// console.log("routes SECRET: ", SECRET); - +dotenv.config(); +const SECRET = process.env.SECRET || "toast is the best secret"; const router = express.Router(); //check if user already exists @@ -84,7 +82,6 @@ router.post("/login", async (req, res) => { accessToken: newAccessToken, role: user.role, }); - // res.json({accessToken}); } else { res.status(400).json({ notFound: true, message: "User not found" }); } @@ -104,34 +101,11 @@ router.get("/verify", authenticateUser, isLoggedIn, (req, res) => { //route to get user role router.get("/role", authenticateUser, (req, res) => { res.json({ role: req.user.role }); - // console.log("role: ", req.user.role); }); - - -/* -// delete these later after testing -//add a test blacklist endpoint -router.get("/blacklist", async (req, res) => { - try { - const blacklist = await Blacklist.find(); - res.json(blacklist); - } catch (error) { - res.status(500).json({ message: "An error occurred while fetching the blacklist" , error: error.message }); - } -}); - -router.post("/blacklist", async (req, res) => { - const { token } = req.body; - const newToken = await new Blacklist({ - token, - }).save(); -}); - -*/ // Route to log out user router.post("/logout", authenticateUser, logoutUser, (req, res) => { - console.log('Logging out user'); // Log a message when the route is hit + console.log("Logging out user"); // Log a message when the route is hit res.json({ message: "You are now logged out" }); }); @@ -156,11 +130,4 @@ router.patch("/users/:id", async (req, res) => { } }); -//route for getting content behind authentication - lets update this with something that makes sense later :) -router.get("/secrets", authenticateUser, isLoggedIn, (req, res) => { - res.send( - "The password is potato - you are authenticated and can see this members only content -lucky you!" - ); -}); - export default router; diff --git a/backend/server.js b/backend/server.js index 6976b2620..b0547edce 100644 --- a/backend/server.js +++ b/backend/server.js @@ -11,25 +11,13 @@ const mongoUrl = process.env.MONGO_URL || "mongodb://localhost/authAPI"; mongoose.connect(mongoUrl); mongoose.Promise = Promise; -const connection = mongoose.connection; - -// Event listeners for the connection for debug -connection.on("connected", () => { - // console.log("Mongoose successfully connected to " + mongoUrl); -}); -// Event listeners for the connection for debug -connection.on("error", (error) => { - // console.log("Mongoose connection error: " + error); -}); - const port = process.env.PORT || 8787; const app = express(); // Add middlewares to enable cors and json body parsing -//allow all for now, restrict later app.use( cors({ - origin: ["*", "http://localhost:5173"], + origin: ["https://auntauthy.netlify.app"], credentials: true, methods: ["GET", "POST", "PATCH", "PUT", "DELETE"], allowedHeaders: ["Content-Type", "Authorization"], @@ -44,7 +32,7 @@ app.get("/", (req, res) => { try { const endpoints = listEndpoints(router); const updatedEndpoints = endpoints.map((endpoint) => { - if ((endpoint.path === "/") || (endpoint.path === "/admin") ){ + if (endpoint.path === "/" || endpoint.path === "/admin") { return { path: endpoint.path, methods: endpoint.methods, From 96bf447d1afd53b05d4001713ac8008c98834795 Mon Sep 17 00:00:00 2001 From: Kathinka Sewell <4355167+kathinka@users.noreply.github.com> Date: Mon, 27 May 2024 00:00:54 +0200 Subject: [PATCH 09/41] chore: Update CORS origin to include additional URLs and allow multiple methods --- backend/server.js | 2 +- frontend/_redirects | 11 +++++++++++ 2 files changed, 12 insertions(+), 1 deletion(-) create mode 100644 frontend/_redirects diff --git a/backend/server.js b/backend/server.js index b0547edce..4669868d7 100644 --- a/backend/server.js +++ b/backend/server.js @@ -17,7 +17,7 @@ const app = express(); // Add middlewares to enable cors and json body parsing app.use( cors({ - origin: ["https://auntauthy.netlify.app"], + origin: ["https://auntauthy.netlify.app", "https://aunt-authy.onrender.com/", "https://project-auth-pqxu.onrender.com/"], credentials: true, methods: ["GET", "POST", "PATCH", "PUT", "DELETE"], allowedHeaders: ["Content-Type", "Authorization"], diff --git a/frontend/_redirects b/frontend/_redirects new file mode 100644 index 000000000..4b7aba4f7 --- /dev/null +++ b/frontend/_redirects @@ -0,0 +1,11 @@ +/exists https://project-auth-pqxu.onrender.com/exists 200 +/signup https://project-auth-pqxu.onrender.com/signup 200 +/login https://project-auth-pqxu.onrender.com/login 200 +/verify https://project-auth-pqxu.onrender.com/verify 200 +/role https://project-auth-pqxu.onrender.com//role 200 +/logout https://project-auth-pqxu.onrender.com/logout 200 +/users/:id https://project-auth-pqxu.onrender.com/users/:id 200 +/admin/users https://project-auth-pqxu.onrender.com/admin/users 200 +/admin https://project-auth-pqxu.onrender.com/admin 200 +/admin/users https://project-auth-pqxu.onrender.com/admin/users 200 +/admin/users/:id https://project-auth-pqxu.onrender.com/admin/users/:id 200 From f17512f3694056af8b15aa0c09708db10e584960 Mon Sep 17 00:00:00 2001 From: Kathinka Sewell <4355167+kathinka@users.noreply.github.com> Date: Mon, 27 May 2024 00:03:51 +0200 Subject: [PATCH 10/41] build: Update build script to include _redirects file in dist folder --- frontend/package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/frontend/package.json b/frontend/package.json index 9debadaac..7a75bf20d 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -5,7 +5,7 @@ "type": "module", "scripts": { "dev": "vite", - "build": "vite build", + "build": "vite build && cp _redirects dist/", "lint": "eslint . --ext js,jsx --report-unused-disable-directives --max-warnings 0", "preview": "vite preview" }, From b10601267eb92fdab0dc8b554234a273995db8f2 Mon Sep 17 00:00:00 2001 From: Kathinka Sewell <4355167+kathinka@users.noreply.github.com> Date: Mon, 27 May 2024 15:26:18 +0200 Subject: [PATCH 11/41] Update redirect URL for /role in _redirects file --- frontend/_redirects | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/frontend/_redirects b/frontend/_redirects index 4b7aba4f7..cd12647f1 100644 --- a/frontend/_redirects +++ b/frontend/_redirects @@ -2,10 +2,9 @@ /signup https://project-auth-pqxu.onrender.com/signup 200 /login https://project-auth-pqxu.onrender.com/login 200 /verify https://project-auth-pqxu.onrender.com/verify 200 -/role https://project-auth-pqxu.onrender.com//role 200 +/role https://project-auth-pqxu.onrender.com/role 200 /logout https://project-auth-pqxu.onrender.com/logout 200 /users/:id https://project-auth-pqxu.onrender.com/users/:id 200 -/admin/users https://project-auth-pqxu.onrender.com/admin/users 200 /admin https://project-auth-pqxu.onrender.com/admin 200 /admin/users https://project-auth-pqxu.onrender.com/admin/users 200 /admin/users/:id https://project-auth-pqxu.onrender.com/admin/users/:id 200 From d6301052c70b756ea49195c2a19ddb27c4927392 Mon Sep 17 00:00:00 2001 From: Kathinka Sewell <4355167+kathinka@users.noreply.github.com> Date: Mon, 27 May 2024 15:38:14 +0200 Subject: [PATCH 12/41] chore: Update CORS origin to include additional URLs and allow multiple methods --- backend/server.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/backend/server.js b/backend/server.js index 4669868d7..0db44f7a0 100644 --- a/backend/server.js +++ b/backend/server.js @@ -17,7 +17,7 @@ const app = express(); // Add middlewares to enable cors and json body parsing app.use( cors({ - origin: ["https://auntauthy.netlify.app", "https://aunt-authy.onrender.com/", "https://project-auth-pqxu.onrender.com/"], + origin: ["*","https://auntauthy.netlify.app", "https://aunt-authy.onrender.com/", "https://project-auth-pqxu.onrender.com/"], credentials: true, methods: ["GET", "POST", "PATCH", "PUT", "DELETE"], allowedHeaders: ["Content-Type", "Authorization"], From 049577e988b6d0c024fa7e88ddf458b74aedc7c1 Mon Sep 17 00:00:00 2001 From: Kathinka Sewell <4355167+kathinka@users.noreply.github.com> Date: Mon, 27 May 2024 15:55:47 +0200 Subject: [PATCH 13/41] chore: Update CORS origin to include additional URLs and allow multiple methods --- backend/server.js | 23 +++++++++++++++-------- frontend/netlify.toml | 4 ++++ 2 files changed, 19 insertions(+), 8 deletions(-) create mode 100644 frontend/netlify.toml diff --git a/backend/server.js b/backend/server.js index 0db44f7a0..8d5fd1ada 100644 --- a/backend/server.js +++ b/backend/server.js @@ -14,15 +14,22 @@ mongoose.Promise = Promise; const port = process.env.PORT || 8787; const app = express(); + +const allowedOrigins = ["https://auntauthy.netlify.app", "https://aunt-authy.onrender.com", "https://project-auth-pqxu.onrender.com"]; // Add middlewares to enable cors and json body parsing -app.use( - cors({ - origin: ["*","https://auntauthy.netlify.app", "https://aunt-authy.onrender.com/", "https://project-auth-pqxu.onrender.com/"], - credentials: true, - methods: ["GET", "POST", "PATCH", "PUT", "DELETE"], - allowedHeaders: ["Content-Type", "Authorization"], - }) -); +app.use(cors({ + origin: function (origin, callback) { + // Allow requests with no origin + // (like mobile apps or curl requests) + if(!origin) return callback(null, true); + if(allowedOrigins.indexOf(origin) === -1){ + var msg = 'The CORS policy for this site does not ' + + 'allow access from the specified Origin.'; + return callback(new Error(msg), false); + } + return callback(null, true); + } +})); app.use(express.json()); app.use("/", router); diff --git a/frontend/netlify.toml b/frontend/netlify.toml new file mode 100644 index 000000000..32464b375 --- /dev/null +++ b/frontend/netlify.toml @@ -0,0 +1,4 @@ +[build] + base = "frontend/" + publish = "dist" + command = "npm run build" \ No newline at end of file From d702362a831fce59c33472dbfedd110705b6e198 Mon Sep 17 00:00:00 2001 From: Kathinka Sewell <4355167+kathinka@users.noreply.github.com> Date: Mon, 27 May 2024 16:25:22 +0200 Subject: [PATCH 14/41] Refactor Middleware.js to use ES6 module syntax --- backend/middleware/Middleware.js | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/backend/middleware/Middleware.js b/backend/middleware/Middleware.js index 5142c5f87..92059a437 100644 --- a/backend/middleware/Middleware.js +++ b/backend/middleware/Middleware.js @@ -107,8 +107,7 @@ const isLoggedIn = (req, res, next) => { //tell the user they are not logged in - those invalidated tokens are dead to us - begone ghost! res.status(403).json({ message: "No user logged in" }); } - // give success message if user is logged in - you are in the club, party on! 🎉 - res.json({ message: "You are logged in" }); + }; -module.exports = { authenticateUser, authorizeUser, logoutUser, isLoggedIn }; +export { authenticateUser, authorizeUser, logoutUser, isLoggedIn }; From 2019ed0ddf418b577ee26d10eb4e2d279a0f2101 Mon Sep 17 00:00:00 2001 From: Kathinka Sewell <4355167+kathinka@users.noreply.github.com> Date: Mon, 27 May 2024 16:37:22 +0200 Subject: [PATCH 15/41] Update the fetch URL in the ProtectedRoutes.jsx file to use HTTPS instead of the relative path '/verify'. to ensures that the request is made securely. --- frontend/components/ProtectedRoutes.jsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/frontend/components/ProtectedRoutes.jsx b/frontend/components/ProtectedRoutes.jsx index 53957affb..ac3034b40 100644 --- a/frontend/components/ProtectedRoutes.jsx +++ b/frontend/components/ProtectedRoutes.jsx @@ -11,7 +11,7 @@ export const ProtectedRoute = ({ children }) => { useEffect(() => { const verifyToken = async () => { try { - const response = await fetch('/verify', { + const response = await fetch('https://project-auth-pqxu.onrender.com/verify', { method: 'GET', headers: { 'Content-Type': 'application/json', From eed23ef62378c82b6fa2028b8bd96e802fc85967 Mon Sep 17 00:00:00 2001 From: Kathinka Sewell <4355167+kathinka@users.noreply.github.com> Date: Mon, 27 May 2024 16:41:05 +0200 Subject: [PATCH 16/41] Update fetch URL in ProtectedRoutes.jsx to use HTTPS for secure requests --- frontend/components/ProtectedRoutes.jsx | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/frontend/components/ProtectedRoutes.jsx b/frontend/components/ProtectedRoutes.jsx index ac3034b40..16da065ba 100644 --- a/frontend/components/ProtectedRoutes.jsx +++ b/frontend/components/ProtectedRoutes.jsx @@ -7,11 +7,13 @@ export const ProtectedRoute = ({ children }) => { const navigate = useNavigate(); const [isLoading, setIsLoading] = useState(true); const token = sessionStorage.getItem('token'); + const apiKey = import.meta.env.VITE_API_KEY; + const API = apiKey + "/verify"; useEffect(() => { const verifyToken = async () => { try { - const response = await fetch('https://project-auth-pqxu.onrender.com/verify', { + const response = await fetch(API, { method: 'GET', headers: { 'Content-Type': 'application/json', From 3d3ad3dff8a10ae6eae1d31f696ce2e81e5186e8 Mon Sep 17 00:00:00 2001 From: Kathinka Sewell <4355167+kathinka@users.noreply.github.com> Date: Mon, 27 May 2024 16:47:55 +0200 Subject: [PATCH 17/41] chore: Update isLoggedIn middleware to asynchronously check token blacklist --- backend/middleware/Middleware.js | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/backend/middleware/Middleware.js b/backend/middleware/Middleware.js index 92059a437..c1280b299 100644 --- a/backend/middleware/Middleware.js +++ b/backend/middleware/Middleware.js @@ -91,14 +91,14 @@ const logoutUser = async (req, res, next) => { }; //check if user is logged in -const isLoggedIn = (req, res, next) => { +const isLoggedIn = async (req, res, next) => { if (req.user) { // Check if the user's token is in the blacklist, and if it is, tell them to get lost tho shall not pass - if (tokenBlacklist.includes(req.user.accessToken)) { + const blacklist = await tokenBlacklist; // If tokenBlacklist is a promise + if (blacklist.includes(req.user.accessToken)) { return ( res .status(401) - //tell the user they are not allowed to pass - like a bouncer at a club .json({ message: "This token has been invalidated" }) ); } From 7b333fe10c0b16697c6e5a215ec96ba17be95239 Mon Sep 17 00:00:00 2001 From: Kathinka Sewell <4355167+kathinka@users.noreply.github.com> Date: Mon, 27 May 2024 17:04:10 +0200 Subject: [PATCH 18/41] more logging --- backend/middleware/Middleware.js | 48 +++++++++++++++++++------------- backend/server.js | 1 + 2 files changed, 30 insertions(+), 19 deletions(-) diff --git a/backend/middleware/Middleware.js b/backend/middleware/Middleware.js index c1280b299..0433e0689 100644 --- a/backend/middleware/Middleware.js +++ b/backend/middleware/Middleware.js @@ -19,6 +19,7 @@ const authenticateUser = async (req, res, next) => { if (bearer.length === 2 && bearer[0] === "Bearer") { const bearerToken = bearer[1]; try { + //decode the token 🤫 const decoded = jwt.verify(bearerToken, SECRET); // find user with the decoded id @@ -28,6 +29,7 @@ const authenticateUser = async (req, res, next) => { req.user = user; req.user.accessToken = bearerToken; // Set the access token for the user //move to the next middleware -> authorizeUser or isLoggedIn to see if token is valid (not stored in blacklist) + console.log(res.getHeaders()); next(); } else { res.status(401).json({ @@ -53,6 +55,7 @@ const authenticateUser = async (req, res, next) => { // authorize user const authorizeUser = (roles) => { return (req, res, next) => { + //check if user role is in the roles array and can hang out with the cool kids if (!roles.includes(req.user.role)) { return res.status(403).json({ @@ -61,16 +64,42 @@ const authorizeUser = (roles) => { )}`, }); } + console.log(res.getHeaders()); next(); }; }; + +//check if user is logged in +const isLoggedIn = async (req, res, next) => { + if (req.user) { + + // Check if the user's token is in the blacklist, and if it is, tell them to get lost tho shall not pass + const blacklist = await tokenBlacklist; // If tokenBlacklist is a promise + if (blacklist.includes(req.user.accessToken)) { + return ( + res + .status(401) + .json({ message: "This token has been invalidated" }) + ); + } + console.log(res.getHeaders()); + next(); + } else { + //tell the user they are not logged in - those invalidated tokens are dead to us - begone ghost! + res.status(403).json({ message: "No user logged in" }); + } + +}; + + //log out user const logoutUser = async (req, res, next) => { try { if (!req.user || !req.user.accessToken) { return res.status(400).json({ message: "Invalid user or access token" }); } + // Add the token to the blacklist, so it can't be used again const token = req.user.accessToken; const blacklistEntry = new Blacklist({ token: token }); @@ -90,24 +119,5 @@ const logoutUser = async (req, res, next) => { } }; -//check if user is logged in -const isLoggedIn = async (req, res, next) => { - if (req.user) { - // Check if the user's token is in the blacklist, and if it is, tell them to get lost tho shall not pass - const blacklist = await tokenBlacklist; // If tokenBlacklist is a promise - if (blacklist.includes(req.user.accessToken)) { - return ( - res - .status(401) - .json({ message: "This token has been invalidated" }) - ); - } - next(); - } else { - //tell the user they are not logged in - those invalidated tokens are dead to us - begone ghost! - res.status(403).json({ message: "No user logged in" }); - } - -}; export { authenticateUser, authorizeUser, logoutUser, isLoggedIn }; diff --git a/backend/server.js b/backend/server.js index 8d5fd1ada..1dc762d95 100644 --- a/backend/server.js +++ b/backend/server.js @@ -19,6 +19,7 @@ const allowedOrigins = ["https://auntauthy.netlify.app", "https://aunt-authy.onr // Add middlewares to enable cors and json body parsing app.use(cors({ origin: function (origin, callback) { + console.log('Origin:', origin); // Allow requests with no origin // (like mobile apps or curl requests) if(!origin) return callback(null, true); From 19d38d041f3391892c2e0457fa5535fc3a2608cb Mon Sep 17 00:00:00 2001 From: Kathinka Sewell <4355167+kathinka@users.noreply.github.com> Date: Mon, 27 May 2024 17:08:37 +0200 Subject: [PATCH 19/41] added handling of successful verification of token --- frontend/components/ProtectedRoutes.jsx | 51 +++++++++++-------------- 1 file changed, 23 insertions(+), 28 deletions(-) diff --git a/frontend/components/ProtectedRoutes.jsx b/frontend/components/ProtectedRoutes.jsx index 16da065ba..aee1e9488 100644 --- a/frontend/components/ProtectedRoutes.jsx +++ b/frontend/components/ProtectedRoutes.jsx @@ -2,7 +2,6 @@ import { useNavigate } from 'react-router-dom'; import PropTypes from 'prop-types'; import { useEffect, useState } from 'react'; -// ProtectedRoute component handles the verification of the token for all protected routes export const ProtectedRoute = ({ children }) => { const navigate = useNavigate(); const [isLoading, setIsLoading] = useState(true); @@ -10,37 +9,33 @@ export const ProtectedRoute = ({ children }) => { const apiKey = import.meta.env.VITE_API_KEY; const API = apiKey + "/verify"; - useEffect(() => { - const verifyToken = async () => { - try { - const response = await fetch(API, { - method: 'GET', - headers: { - 'Content-Type': 'application/json', - "Authorization": `Bearer ${token}`, - }, - }); - - if (!response.ok) { - throw new Error('Token verification failed'); - } - - setIsLoading(false); - } catch (error) { - sessionStorage.removeItem('token'); - navigate('/home'); + const verifyToken = async () => { + try { + const response = await fetch(API, { + method: 'GET', + headers: { + 'Content-Type': 'application/json', + "Authorization": `Bearer ${token}`, + }, + }); + + if (!response.ok) { + throw new Error('Token verification failed'); } - }; - if (token) { - verifyToken(); - } else { - navigate('/home'); + setIsLoading(false); + } catch (error) { + console.error(error); + navigate('/login'); } - }, [navigate, token]); + }; + + useEffect(() => { + verifyToken(); + }, []); - if (isLoading || !token) { - return null; + if (isLoading) { + return
Loading...
; } return children; From 585294216330e5c7bcb2e4b3666e55683cd2db1e Mon Sep 17 00:00:00 2001 From: Kathinka Sewell <4355167+kathinka@users.noreply.github.com> Date: Mon, 27 May 2024 17:15:51 +0200 Subject: [PATCH 20/41] adjust the token verification function --- frontend/components/ProtectedRoutes.jsx | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/frontend/components/ProtectedRoutes.jsx b/frontend/components/ProtectedRoutes.jsx index aee1e9488..16cbf8d81 100644 --- a/frontend/components/ProtectedRoutes.jsx +++ b/frontend/components/ProtectedRoutes.jsx @@ -10,6 +10,11 @@ export const ProtectedRoute = ({ children }) => { const API = apiKey + "/verify"; const verifyToken = async () => { + if (!token) { + navigate('/login'); + return; + } + try { const response = await fetch(API, { method: 'GET', From d6e836a2cdb26d64c413f5c491250c2eb10504bb Mon Sep 17 00:00:00 2001 From: Kathinka Sewell <4355167+kathinka@users.noreply.github.com> Date: Mon, 27 May 2024 17:23:24 +0200 Subject: [PATCH 21/41] chore: Update CORS origin to allow all origins --- backend/server.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/backend/server.js b/backend/server.js index 1dc762d95..4f1d67cfc 100644 --- a/backend/server.js +++ b/backend/server.js @@ -15,7 +15,7 @@ const port = process.env.PORT || 8787; const app = express(); -const allowedOrigins = ["https://auntauthy.netlify.app", "https://aunt-authy.onrender.com", "https://project-auth-pqxu.onrender.com"]; +const allowedOrigins = ["*"]; // Add middlewares to enable cors and json body parsing app.use(cors({ origin: function (origin, callback) { From 9ac22af77f1de47e508444a40e154b967c94845f Mon Sep 17 00:00:00 2001 From: Kathinka Sewell <4355167+kathinka@users.noreply.github.com> Date: Mon, 27 May 2024 17:39:14 +0200 Subject: [PATCH 22/41] Fix token blacklist check logic in isLoggedIn middleware --- backend/middleware/Middleware.js | 2 +- backend/server.js | 6 ++++-- 2 files changed, 5 insertions(+), 3 deletions(-) diff --git a/backend/middleware/Middleware.js b/backend/middleware/Middleware.js index 0433e0689..27de989d7 100644 --- a/backend/middleware/Middleware.js +++ b/backend/middleware/Middleware.js @@ -76,7 +76,7 @@ const isLoggedIn = async (req, res, next) => { // Check if the user's token is in the blacklist, and if it is, tell them to get lost tho shall not pass const blacklist = await tokenBlacklist; // If tokenBlacklist is a promise - if (blacklist.includes(req.user.accessToken)) { + if (!blacklist.includes(req.user.accessToken)) { return ( res .status(401) diff --git a/backend/server.js b/backend/server.js index 4f1d67cfc..f222de588 100644 --- a/backend/server.js +++ b/backend/server.js @@ -14,8 +14,8 @@ mongoose.Promise = Promise; const port = process.env.PORT || 8787; const app = express(); - -const allowedOrigins = ["*"]; +/* +const allowedOrigins = ["https://auntauthy.netlify.app", "https://aunt-authy.onrender.com", "https://project-auth-pqxu.onrender.com"]; // Add middlewares to enable cors and json body parsing app.use(cors({ origin: function (origin, callback) { @@ -31,7 +31,9 @@ app.use(cors({ return callback(null, true); } })); +*/ +app.use(cors()); app.use(express.json()); app.use("/", router); app.use("/admin", adminRouter); From 0507ee1c1ce8742ba560feedf4fda07dc9ea78e2 Mon Sep 17 00:00:00 2001 From: Kathinka Sewell <4355167+kathinka@users.noreply.github.com> Date: Mon, 27 May 2024 18:02:13 +0200 Subject: [PATCH 23/41] refactor Middleware.js to improve token handling and verification again --- backend/middleware/Middleware.js | 26 +++++++++++++------------- 1 file changed, 13 insertions(+), 13 deletions(-) diff --git a/backend/middleware/Middleware.js b/backend/middleware/Middleware.js index 27de989d7..472e761c8 100644 --- a/backend/middleware/Middleware.js +++ b/backend/middleware/Middleware.js @@ -19,7 +19,6 @@ const authenticateUser = async (req, res, next) => { if (bearer.length === 2 && bearer[0] === "Bearer") { const bearerToken = bearer[1]; try { - //decode the token 🤫 const decoded = jwt.verify(bearerToken, SECRET); // find user with the decoded id @@ -55,7 +54,6 @@ const authenticateUser = async (req, res, next) => { // authorize user const authorizeUser = (roles) => { return (req, res, next) => { - //check if user role is in the roles array and can hang out with the cool kids if (!roles.includes(req.user.role)) { return res.status(403).json({ @@ -69,19 +67,24 @@ const authorizeUser = (roles) => { }; }; - //check if user is logged in const isLoggedIn = async (req, res, next) => { if (req.user) { - // Check if the user's token is in the blacklist, and if it is, tell them to get lost tho shall not pass - const blacklist = await tokenBlacklist; // If tokenBlacklist is a promise + const blacklist = await tokenBlacklist; + + // Check if blacklist is undefined + if (!blacklist) { + return res + .status(500) + .json({ message: "Error retrieving token blacklist" }); + } + + // Check if the user's token is in the blacklist if (!blacklist.includes(req.user.accessToken)) { - return ( - res - .status(401) - .json({ message: "This token has been invalidated" }) - ); + return res + .status(401) + .json({ message: "This token has been invalidated" }); } console.log(res.getHeaders()); next(); @@ -89,10 +92,8 @@ const isLoggedIn = async (req, res, next) => { //tell the user they are not logged in - those invalidated tokens are dead to us - begone ghost! res.status(403).json({ message: "No user logged in" }); } - }; - //log out user const logoutUser = async (req, res, next) => { try { @@ -119,5 +120,4 @@ const logoutUser = async (req, res, next) => { } }; - export { authenticateUser, authorizeUser, logoutUser, isLoggedIn }; From dc1cb89070fd118fff92b675605f3c4a7814d072 Mon Sep 17 00:00:00 2001 From: Kathinka Sewell <4355167+kathinka@users.noreply.github.com> Date: Tue, 28 May 2024 00:23:05 +0200 Subject: [PATCH 24/41] fixed a bug with getting the accesstoken and verifying with thew blacklist --- backend/middleware/Middleware.js | 36 +++++++++++--------- backend/routes/adminRoutes.js | 7 +++- backend/routes/routes.js | 8 ++++- frontend/components/Dashboard.jsx | 5 +-- frontend/components/Login.jsx | 2 +- frontend/components/ProtectedRoutes.jsx | 6 +++- frontend/components/forms/UpdateUser.jsx | 5 ++- frontend/components/forms/UpdateUserRole.jsx | 3 +- frontend/routes/AppRoutes.jsx | 9 +++-- 9 files changed, 52 insertions(+), 29 deletions(-) diff --git a/backend/middleware/Middleware.js b/backend/middleware/Middleware.js index 472e761c8..dab004f1c 100644 --- a/backend/middleware/Middleware.js +++ b/backend/middleware/Middleware.js @@ -21,8 +21,10 @@ const authenticateUser = async (req, res, next) => { try { //decode the token 🤫 const decoded = jwt.verify(bearerToken, SECRET); + console.log("Decoded token: ", decoded); // Log the decoded token // find user with the decoded id const user = await User.findById(decoded.id); + console.log("Decoded token2: ", decoded); // Log the decoded token //check if user exists and ready to party like a request object if (user) { req.user = user; @@ -37,6 +39,7 @@ const authenticateUser = async (req, res, next) => { }); } } catch (err) { + console.error("Token verification error: ", err); // Log the error from jwt.verify() res.status(403).json({ loggedOut: true, message: "Invalid token" }); } } else { @@ -67,29 +70,30 @@ const authorizeUser = (roles) => { }; }; -//check if user is logged in +//check if user is logged in and the token is not in the blacklist const isLoggedIn = async (req, res, next) => { if (req.user) { - // Check if the user's token is in the blacklist, and if it is, tell them to get lost tho shall not pass - const blacklist = await tokenBlacklist; + try { + // Query the database to check if the token exists in the blacklist + const tokenInBlacklist = await Blacklist.findOne({ token: req.user.accessToken }); - // Check if blacklist is undefined - if (!blacklist) { - return res - .status(500) - .json({ message: "Error retrieving token blacklist" }); - } + // If the token is in the blacklist, return a 401 status + if (tokenInBlacklist) { + return res + .status(401) + .json({ message: "This token has been invalidated" }); + } - // Check if the user's token is in the blacklist - if (!blacklist.includes(req.user.accessToken)) { + // If the token is not in the blacklist, proceed to the next middleware function or route handler + next(); + } catch (error) { + // If there's an error while querying the database, return a 500 status with a detailed error message return res - .status(401) - .json({ message: "This token has been invalidated" }); + .status(500) + .json({ message: "Error retrieving token blacklist", error: error.message }); } - console.log(res.getHeaders()); - next(); } else { - //tell the user they are not logged in - those invalidated tokens are dead to us - begone ghost! + // If the user is not logged in, return a 403 status res.status(403).json({ message: "No user logged in" }); } }; diff --git a/backend/routes/adminRoutes.js b/backend/routes/adminRoutes.js index 71cd3a0c8..14867414a 100644 --- a/backend/routes/adminRoutes.js +++ b/backend/routes/adminRoutes.js @@ -1,8 +1,13 @@ import express from "express"; import User from "../model/user-model"; +import Blacklist from "../model/blacklist-model" import bcrypt from "bcrypt"; import { authenticateUser, authorizeUser } from "../middleware/Middleware"; +import dotenv from "dotenv"; +import jwt from 'jsonwebtoken'; +dotenv.config(); +const SECRET = process.env.SECRET || "toast is the best secret"; const adminRouter = express.Router(); adminRouter.use(authenticateUser, authorizeUser(["admin"])); @@ -20,7 +25,7 @@ adminRouter.get("/users", async (req, res) => { }); // route for getting content behind authorization find me at /admin -adminRouter.get("/", authenticateUser, authorizeUser(["admin"]), (req, res) => { +adminRouter.get("/", (req, res) => { // This code will only run if the user is an admin res.render("admin"); }); diff --git a/backend/routes/routes.js b/backend/routes/routes.js index 7a754365c..b13ca347c 100644 --- a/backend/routes/routes.js +++ b/backend/routes/routes.js @@ -94,10 +94,16 @@ router.post("/login", async (req, res) => { }); //route to verify if token is valid with middleware isLoggedIn -router.get("/verify", authenticateUser, isLoggedIn, (req, res) => { +router.get("/verify", authenticateUser,(req, res) => { res.json({ message: "You are logged in" }); }); + +//route to verify if token is valid with middleware isLoggedIn +//router.get("/verify", authenticateUser, isLoggedIn,(req, res) => { +// res.json({ message: "You are logged in" }); +//}); + //route to get user role router.get("/role", authenticateUser, (req, res) => { res.json({ role: req.user.role }); diff --git a/frontend/components/Dashboard.jsx b/frontend/components/Dashboard.jsx index 2de40eeaa..a594a30e8 100644 --- a/frontend/components/Dashboard.jsx +++ b/frontend/components/Dashboard.jsx @@ -7,10 +7,11 @@ export const Dashboard = () => { const navigate = useNavigate(); const apiKey = import.meta.env.VITE_API_KEY; const API = apiKey + "/role"; - const token = sessionStorage.getItem('token'); + useEffect(() => { const fetchData = async () => { + const token = sessionStorage.getItem('token'); await fetch(`${API}`, { method: 'GET', headers: { @@ -34,7 +35,7 @@ export const Dashboard = () => { fetchData(); } - , [token, API]); + , []); return ( <> diff --git a/frontend/components/Login.jsx b/frontend/components/Login.jsx index abb5ebed9..1f62c230e 100644 --- a/frontend/components/Login.jsx +++ b/frontend/components/Login.jsx @@ -27,7 +27,7 @@ export const Login = () => { }) .then((json) => { setMessage("Login successful!"); - navigate("/"); + navigate("/dashboard"); // Store the token in session storage sessionStorage.setItem('token', json.accessToken); }) diff --git a/frontend/components/ProtectedRoutes.jsx b/frontend/components/ProtectedRoutes.jsx index 16cbf8d81..1a521dbc0 100644 --- a/frontend/components/ProtectedRoutes.jsx +++ b/frontend/components/ProtectedRoutes.jsx @@ -5,11 +5,15 @@ import { useEffect, useState } from 'react'; export const ProtectedRoute = ({ children }) => { const navigate = useNavigate(); const [isLoading, setIsLoading] = useState(true); - const token = sessionStorage.getItem('token'); + const apiKey = import.meta.env.VITE_API_KEY; const API = apiKey + "/verify"; const verifyToken = async () => { + const token = sessionStorage.getItem('token'); + console.log("Token from session storage: ", token); + console.log(API); + if (!token) { navigate('/login'); return; diff --git a/frontend/components/forms/UpdateUser.jsx b/frontend/components/forms/UpdateUser.jsx index ef4330b2d..21b2f93e7 100644 --- a/frontend/components/forms/UpdateUser.jsx +++ b/frontend/components/forms/UpdateUser.jsx @@ -2,7 +2,6 @@ import { useState } from 'react'; const apiKey = import.meta.env.VITE_API_KEY; const API = apiKey + "/admin" -const token = sessionStorage.getItem('token'); export const UpdateUser = () => { const [id, setId] = useState(''); @@ -11,15 +10,19 @@ export const UpdateUser = () => { const [role, setRole] = useState('user'); const [password, setPassword] = useState(''); const [errors, setErrors] = useState({}); + const Update = async () => { + const token = sessionStorage.getItem('token'); console.log('Token:', token); // Log the token try { + console.log('API:', API); // Log the API const response = await fetch(`${API}/users/${id}`, { method: 'PUT', headers: { 'Content-Type': 'application/json', 'Authorization': `Bearer ${token}` }, + body: JSON.stringify({ name, email, role, password }), }); if (!response.ok) { diff --git a/frontend/components/forms/UpdateUserRole.jsx b/frontend/components/forms/UpdateUserRole.jsx index 44bcf4963..dc64e21d9 100644 --- a/frontend/components/forms/UpdateUserRole.jsx +++ b/frontend/components/forms/UpdateUserRole.jsx @@ -8,9 +8,10 @@ export const UpdateUserRole = () => { const apiKey = import.meta.env.VITE_API_KEY; const API = apiKey + "/admin"; console.log(API); - const token = sessionStorage.getItem('token'); + const updateRole = async (e) => { + const token = sessionStorage.getItem('token'); e.preventDefault(); const id = e.target.id.value; const role = e.target.role.value; diff --git a/frontend/routes/AppRoutes.jsx b/frontend/routes/AppRoutes.jsx index 8d78b5f85..cf13c7047 100644 --- a/frontend/routes/AppRoutes.jsx +++ b/frontend/routes/AppRoutes.jsx @@ -12,13 +12,12 @@ export const AppRoutes = () => { return ( - - } fallbackComponent={} /> - } fallbackComponent={} /> - } /> - } /> + } fallbackComponent={} /> } /> } /> + } /> + } fallbackComponent={} /> + } /> From 9930dcd3629abb7c174d660e1382fb3cb1dd712c Mon Sep 17 00:00:00 2001 From: Kathinka Sewell <4355167+kathinka@users.noreply.github.com> Date: Tue, 28 May 2024 18:40:17 +0200 Subject: [PATCH 25/41] improve code readability, added better user feedback, and more responsive lists in the admin panel when the admin user changes userinfo --- backend/middleware/Middleware.js | 9 +-- backend/routes/adminRoutes.js | 36 ++++----- backend/routes/routes.js | 1 - backend/server.js | 81 +++++++++++++------- frontend/components/Admin.jsx | 11 +-- frontend/components/Logout.jsx | 6 +- frontend/components/ProtectedRoutes.jsx | 4 +- frontend/components/forms/CreateUser.jsx | 19 +++-- frontend/components/forms/DeleteUser.jsx | 12 ++- frontend/components/forms/UpdateUser.jsx | 62 ++++++++++----- frontend/components/forms/UpdateUserRole.jsx | 20 ++--- 11 files changed, 155 insertions(+), 106 deletions(-) diff --git a/backend/middleware/Middleware.js b/backend/middleware/Middleware.js index dab004f1c..d16e3b96a 100644 --- a/backend/middleware/Middleware.js +++ b/backend/middleware/Middleware.js @@ -1,12 +1,11 @@ import User from "../model/user-model"; import dotenv from "dotenv"; import Blacklist from "../model/blacklist-model"; -import { tokenBlacklist } from "../utility/tokenBlacklist"; import jwt from "jsonwebtoken"; dotenv.config(); const SECRET = process.env.SECRET || "toast is the best secret"; -//console.log("middleware SECRET: ", SECRET); + const authenticateUser = async (req, res, next) => { // Get the token from the request headers @@ -21,16 +20,13 @@ const authenticateUser = async (req, res, next) => { try { //decode the token 🤫 const decoded = jwt.verify(bearerToken, SECRET); - console.log("Decoded token: ", decoded); // Log the decoded token // find user with the decoded id const user = await User.findById(decoded.id); - console.log("Decoded token2: ", decoded); // Log the decoded token //check if user exists and ready to party like a request object if (user) { req.user = user; req.user.accessToken = bearerToken; // Set the access token for the user //move to the next middleware -> authorizeUser or isLoggedIn to see if token is valid (not stored in blacklist) - console.log(res.getHeaders()); next(); } else { res.status(401).json({ @@ -39,7 +35,6 @@ const authenticateUser = async (req, res, next) => { }); } } catch (err) { - console.error("Token verification error: ", err); // Log the error from jwt.verify() res.status(403).json({ loggedOut: true, message: "Invalid token" }); } } else { @@ -65,7 +60,6 @@ const authorizeUser = (roles) => { )}`, }); } - console.log(res.getHeaders()); next(); }; }; @@ -116,7 +110,6 @@ const logoutUser = async (req, res, next) => { res.json({ message: "You are now logged out" }); } catch (error) { // if we can let that user go, we will let them know - but if we can't, we will let the console know - console.error("Error logging out user: ", error); res.status(500).json({ message: "An error occurred while logging out", error: error.message, diff --git a/backend/routes/adminRoutes.js b/backend/routes/adminRoutes.js index 14867414a..80da7e7e4 100644 --- a/backend/routes/adminRoutes.js +++ b/backend/routes/adminRoutes.js @@ -1,10 +1,10 @@ import express from "express"; import User from "../model/user-model"; -import Blacklist from "../model/blacklist-model" +//import Blacklist from "../model/blacklist-model" import bcrypt from "bcrypt"; import { authenticateUser, authorizeUser } from "../middleware/Middleware"; import dotenv from "dotenv"; -import jwt from 'jsonwebtoken'; +//import jwt from 'jsonwebtoken'; dotenv.config(); const SECRET = process.env.SECRET || "toast is the best secret"; @@ -62,28 +62,28 @@ adminRouter.put("/users/:id", async (req, res) => { const { id } = req.params; const { name, email, role, password } = req.body; - if (!name || !email || !role || !password) { + if (!(name || email || role || password)) { return res .status(400) - .json({ error: "All fields are required", error: error.message }); + .json({ + error: "At least noe field is required to update user data", + error: error.message, + }); } - const salt = bcrypt.genSaltSync(); - const updatedUser = await User.findByIdAndUpdate( - id, - { - name, - email, - role, - password: bcrypt.hashSync(password, salt), - }, - { new: true } - ); + let update = {}; + if (name) update.name = name; + if (email) update.email = email; + if (role) update.role = role; + if (password) { + const salt = bcrypt.genSaltSync(); + update.password = bcrypt.hashSync(password, salt); + } + + const updatedUser = await User.findByIdAndUpdate(id, update, { new: true }); if (!updatedUser) { - return res - .status(404) - .json({ error: "User not found", error: error.message }); + return res.status(404).json({ error: "User not found" }); } res.json(updatedUser); diff --git a/backend/routes/routes.js b/backend/routes/routes.js index b13ca347c..681b9cf9e 100644 --- a/backend/routes/routes.js +++ b/backend/routes/routes.js @@ -111,7 +111,6 @@ router.get("/role", authenticateUser, (req, res) => { // Route to log out user router.post("/logout", authenticateUser, logoutUser, (req, res) => { - console.log("Logging out user"); // Log a message when the route is hit res.json({ message: "You are now logged out" }); }); diff --git a/backend/server.js b/backend/server.js index f222de588..3a4f75ee3 100644 --- a/backend/server.js +++ b/backend/server.js @@ -14,44 +14,69 @@ mongoose.Promise = Promise; const port = process.env.PORT || 8787; const app = express(); -/* -const allowedOrigins = ["https://auntauthy.netlify.app", "https://aunt-authy.onrender.com", "https://project-auth-pqxu.onrender.com"]; -// Add middlewares to enable cors and json body parsing -app.use(cors({ - origin: function (origin, callback) { - console.log('Origin:', origin); - // Allow requests with no origin - // (like mobile apps or curl requests) - if(!origin) return callback(null, true); - if(allowedOrigins.indexOf(origin) === -1){ - var msg = 'The CORS policy for this site does not ' + - 'allow access from the specified Origin.'; - return callback(new Error(msg), false); - } - return callback(null, true); - } -})); -*/ +const allowedOrigins = [ + "https://auntauthy.netlify.app", + "https://aunt-authy.onrender.com", + "http://localhost:5173", + "http://localhost:8787", +]; + +app.use( + cors({ + origin: function (origin, callback) { + // Allow requests with no origin + // (like mobile apps or curl requests) + if (!origin) return callback(null, true); + if (allowedOrigins.indexOf(origin) === -1) { + var msg = + "The CORS policy for this site does not " + + "allow access from the specified Origin."; + return callback(new Error(msg), false); + } + return callback(null, true); + }, + }) +); -app.use(cors()); app.use(express.json()); app.use("/", router); app.use("/admin", adminRouter); app.get("/", (req, res) => { try { - const endpoints = listEndpoints(router); - const updatedEndpoints = endpoints.map((endpoint) => { - if (endpoint.path === "/" || endpoint.path === "/admin") { - return { - path: endpoint.path, - methods: endpoint.methods, - queryParameters: [], - }; - } + const routerEndpoints = listEndpoints(router); + const adminRouterEndpoints = listEndpoints(adminRouter); + const allEndpoints = [...routerEndpoints, ...adminRouterEndpoints]; + + const descriptions = { + "/admin/users": + "This route retrieves all users from the database. It requires admin authorization.", + "/admin": + "This route renders the admin page. It requires admin authorization.", + "/exists": + "This route checks if a user with the provided email already exists in the database. It expects a JSON body with an 'email' field.", + "/signup": + "This route creates a new user with the provided name, email, and password. The password is hashed before being stored in the database. Upon successful creation, it returns the new user's ID and a JWT access token.", + "/login": + "This route logs in a user with the provided email and password. If the email and password match a user in the database, it returns the user's ID, a new JWT access token, and the user's role.", + "/verify": + "This route verifies if a user is authenticated by checking their JWT access token. and if their token is not stored in our blacklist. If the token is valid, it returns a message saying the user is logged in.", + "/role": "This route returns the role of the authenticated user.", + "/logout": + "This route logs out the authenticated user. It invalidates the user's JWT access token.", + "/users/:id": + "This route updates the user with the provided ID. If the user is found and updated successfully, it returns the updated user's data.", + "/admin/users/:id": + "This route updates the user with the provided ID. It requires admin authorization. If the user is found and updated successfully, it returns the updated user's data.", + "/users": "This route retrieves all users from the database.", + "/": "This route retrieves all endpoints available in the API.", + }; + + const updatedEndpoints = allEndpoints.map((endpoint) => { return { path: endpoint.path, methods: endpoint.methods, + description: descriptions[endpoint.path] || "No description provided", }; }); res.json(updatedEndpoints); diff --git a/frontend/components/Admin.jsx b/frontend/components/Admin.jsx index 51907b352..482b3073a 100644 --- a/frontend/components/Admin.jsx +++ b/frontend/components/Admin.jsx @@ -6,6 +6,7 @@ import { DeleteUser } from './forms/DeleteUser'; import { CreateUser } from './forms/CreateUser'; import { UpdateUserRole } from './forms/UpdateUserRole'; + export const Admin = () => { const apiKey = import.meta.env.VITE_API_KEY; const API = apiKey + "/admin"; @@ -39,13 +40,13 @@ export const Admin = () => {

Wow - so much admin stuff

As an admin can you do lots of exclusive stuff: create and delete users, update user roles, and userinfo

- - - - + + + + - + {users.map(({ _id, name, email, role }) => (
diff --git a/frontend/components/Logout.jsx b/frontend/components/Logout.jsx index 1ddda867b..db3c15916 100644 --- a/frontend/components/Logout.jsx +++ b/frontend/components/Logout.jsx @@ -19,15 +19,15 @@ export const Logout = () => { "Authorization": `Bearer ${yourToken}` }, }); - const data = await response.json(); - console.log(data); + //const data = await response.json(); + //console.log(data); // Let's Kon-Mari that token who does not spark joy anymore. it has no purpose and is no longer worthy of our neat session storage sessionStorage.removeItem('token'); setMessage("Logout successful! Please sign in."); navigate("/"); } catch (error) { - console.log(error); + setMessage(error); } }; diff --git a/frontend/components/ProtectedRoutes.jsx b/frontend/components/ProtectedRoutes.jsx index 1a521dbc0..316f71587 100644 --- a/frontend/components/ProtectedRoutes.jsx +++ b/frontend/components/ProtectedRoutes.jsx @@ -5,14 +5,12 @@ import { useEffect, useState } from 'react'; export const ProtectedRoute = ({ children }) => { const navigate = useNavigate(); const [isLoading, setIsLoading] = useState(true); - const apiKey = import.meta.env.VITE_API_KEY; const API = apiKey + "/verify"; + const verifyToken = async () => { const token = sessionStorage.getItem('token'); - console.log("Token from session storage: ", token); - console.log(API); if (!token) { navigate('/login'); diff --git a/frontend/components/forms/CreateUser.jsx b/frontend/components/forms/CreateUser.jsx index 54c3749ed..6e24a6927 100644 --- a/frontend/components/forms/CreateUser.jsx +++ b/frontend/components/forms/CreateUser.jsx @@ -3,13 +3,13 @@ import { useState } from 'react'; const apiKey = import.meta.env.VITE_API_KEY; const API = apiKey + "/admin" -export const CreateUser = () => { +export const CreateUser = ({getUsers}) => { const [name, setName] = useState(''); const [email, setEmail] = useState(''); const [role, setRole] = useState('user'); const [password, setPassword] = useState(''); const [errors, setErrors] = useState({}); - const [serverError, setServerError] = useState(''); + const [message, setMessage] = useState(''); const Update = async () => { const token = sessionStorage.getItem('token'); try { @@ -22,17 +22,22 @@ export const CreateUser = () => { body: JSON.stringify({ name, email, role, password }), }); if (!response.ok) { + setMessage('An error occurred while creating the user.'); const errorData = await response.json(); throw new Error(errorData.error); + + } - const data = await response.json(); - console.log(data); + //const data = await response.json(); + //console.log(data); + getUsers(); + setMessage('User created successfully'); } catch (error) { console.error(error); if (error.message.includes('E11000')) { - setServerError('A user with the same email already exists.'); + setMessage('A user with the same email already exists.'); } else { - setServerError('An unexpected error occurred.'); + setMessage('An unexpected error occurred.'); } } }; @@ -77,7 +82,7 @@ export const CreateUser = () => { setPassword(e.target.value.trim())} /> {errors.password &&

{errors.password}

} - {serverError &&

{serverError}

} + {message &&

{message}

} ); } \ No newline at end of file diff --git a/frontend/components/forms/DeleteUser.jsx b/frontend/components/forms/DeleteUser.jsx index c0dbb61ca..7e3f4ba2d 100644 --- a/frontend/components/forms/DeleteUser.jsx +++ b/frontend/components/forms/DeleteUser.jsx @@ -1,8 +1,10 @@ +import { useState } from "react"; -export const DeleteUser = () => { +export const DeleteUser = ({getUsers}) => { const apiKey = import.meta.env.VITE_API_KEY; const API = apiKey + "/admin"; const token = sessionStorage.getItem('token'); + const [message, setMessage] = useState(""); // Move this line inside the DeleteUser component const Delete = async (e) => { e.preventDefault(); @@ -15,11 +17,14 @@ export const DeleteUser = () => { 'Authorization': `Bearer ${token}` }, }); - const data = await response.json(); - console.log(data); + //const data = await response.json(); + //console.log(data); + setMessage('User deleted successfully'); + getUsers(); } catch (error) { console.error(error); + setMessage('An unexpected error occurred.'); } } return ( @@ -28,6 +33,7 @@ export const DeleteUser = () => { + {message &&

{message}

} ); }; diff --git a/frontend/components/forms/UpdateUser.jsx b/frontend/components/forms/UpdateUser.jsx index 21b2f93e7..61f0928d7 100644 --- a/frontend/components/forms/UpdateUser.jsx +++ b/frontend/components/forms/UpdateUser.jsx @@ -1,21 +1,46 @@ -import { useState } from 'react'; +import { useState, useEffect } from 'react'; + const apiKey = import.meta.env.VITE_API_KEY; const API = apiKey + "/admin" -export const UpdateUser = () => { +export const UpdateUser = ({ getUsers }) => { const [id, setId] = useState(''); const [name, setName] = useState(''); const [email, setEmail] = useState(''); const [role, setRole] = useState('user'); const [password, setPassword] = useState(''); const [errors, setErrors] = useState({}); + const [message, setMessage] = useState(''); + + // Store the initial values + const [initialName, setInitialName] = useState(name); + const [initialEmail, setInitialEmail] = useState(email); + const [initialRole, setInitialRole] = useState(role); + const [initialPassword, setInitialPassword] = useState(password); + + // Set the initial values when the component mounts + useEffect(() => { + setInitialName(name); + setInitialEmail(email); + setInitialRole(role); + setInitialPassword(password); + }, []); const Update = async () => { const token = sessionStorage.getItem('token'); - console.log('Token:', token); // Log the token + let userData = {}; + + // Check if any field has changed + if (name === initialName && email === initialEmail && role === initialRole && password === initialPassword) { + setMessage("No changes made to the form, lets try again.😅"); + return; // Stop execution if no fields have changed + } + if (name) userData.name = name; + if (email) userData.email = email; + if (role) userData.role = role; + if (password) userData.password = password; try { - console.log('API:', API); // Log the API const response = await fetch(`${API}/users/${id}`, { method: 'PUT', headers: { @@ -25,11 +50,14 @@ export const UpdateUser = () => { body: JSON.stringify({ name, email, role, password }), }); - if (!response.ok) { - console.log('Response status:', response.status); // Log the response status - } - const data = await response.json(); - console.log(data); + if (response.ok) { + setMessage("User updated successfully"); +getUsers(); + } else + if (!response.ok) { + setErrors("An error occurred while updating the user." + response.statusText); + } + } catch (error) { console.error(error); } @@ -38,14 +66,12 @@ export const UpdateUser = () => { const handleSubmit = (e) => { e.preventDefault(); let errors = {}; - - if (!id) errors.id = "ID is required."; - if (!name) errors.name = "Name is required."; - if (!email) errors.email = "Email is required."; - if (!password) errors.password = "Password is required."; - if (!role) errors.role = "Role is required."; - setErrors(errors) - + //check that at least one field in the form is filled in + if (!name && !email && !password && !role) { + errors.general = "At least one field is required for update."; + setErrors(errors); + return; + } if (Object.keys(errors).length === 0) { // If no errors, celebrate and send the data to the server Update(); @@ -66,7 +92,6 @@ export const UpdateUser = () => { setEmail(e.target.value.trim())} /> {errors.email &&

{errors.email}

} - setPassword(e.target.value.trim())} /> {errors.password &&

{errors.password}

} + {message &&
{message}
} ); } \ No newline at end of file diff --git a/frontend/components/forms/UpdateUserRole.jsx b/frontend/components/forms/UpdateUserRole.jsx index dc64e21d9..4038c122a 100644 --- a/frontend/components/forms/UpdateUserRole.jsx +++ b/frontend/components/forms/UpdateUserRole.jsx @@ -1,13 +1,12 @@ import { useState } from 'react'; -export const UpdateUserRole = () => { +export const UpdateUserRole = ({ getUsers }) => { const [id, setId] = useState(''); const [role, setRole] = useState('user'); - const [error, setError] = useState(''); - const [success, setSuccess] = useState(''); + const [message, setMessage] = useState(''); const apiKey = import.meta.env.VITE_API_KEY; const API = apiKey + "/admin"; - console.log(API); + const updateRole = async (e) => { @@ -17,7 +16,7 @@ export const UpdateUserRole = () => { const role = e.target.role.value; if (!id || !role) { - setError('Please fill in all fields'); + setMessage('Please fill in all fields'); return; } @@ -34,19 +33,15 @@ export const UpdateUserRole = () => { if (!response.ok) { throw new Error('Network response was not ok'); } + setMessage('User role updated successfully'); + getUsers(); - const data = await response.json(); - console.log(data); - setSuccess('User role updated successfully'); } catch (error) { - console.error(error); - setError('An error occurred while updating the user role'); + setMessage('An error occurred while updating the user role'); } }; return (
- {error &&

{error}

} - {success &&

{success}

} setId(e.target.value.trim())} /> @@ -58,6 +53,7 @@ export const UpdateUserRole = () => { + {message &&

{message}

}
); }; \ No newline at end of file From 83c6a1514ed6da1690ba082a5a03db3b261e5fb3 Mon Sep 17 00:00:00 2001 From: Kathinka Sewell <4355167+kathinka@users.noreply.github.com> Date: Tue, 28 May 2024 19:01:58 +0200 Subject: [PATCH 26/41] Improve code readability, add better user feedback, and make admin panel lists more responsive - formatting and cleanup --- backend/middleware/Middleware.js | 12 +++++++----- backend/routes/adminRoutes.js | 14 +++++--------- backend/routes/routes.js | 8 +------- backend/server.js | 4 +--- frontend/components/Admin.jsx | 12 +++++++----- frontend/components/Dashboard.jsx | 4 ++-- frontend/components/Home.jsx | 2 +- frontend/components/Login.jsx | 3 --- frontend/components/Logout.jsx | 4 +--- frontend/components/Registration.jsx | 3 +-- frontend/components/forms/CreateUser.jsx | 7 ++----- frontend/components/forms/DeleteUser.jsx | 7 +++---- frontend/components/forms/UpdateUser.jsx | 5 ++--- 13 files changed, 33 insertions(+), 52 deletions(-) diff --git a/backend/middleware/Middleware.js b/backend/middleware/Middleware.js index d16e3b96a..d8519f350 100644 --- a/backend/middleware/Middleware.js +++ b/backend/middleware/Middleware.js @@ -6,7 +6,6 @@ import jwt from "jsonwebtoken"; dotenv.config(); const SECRET = process.env.SECRET || "toast is the best secret"; - const authenticateUser = async (req, res, next) => { // Get the token from the request headers const bearerHeader = req.headers["authorization"]; @@ -69,7 +68,9 @@ const isLoggedIn = async (req, res, next) => { if (req.user) { try { // Query the database to check if the token exists in the blacklist - const tokenInBlacklist = await Blacklist.findOne({ token: req.user.accessToken }); + const tokenInBlacklist = await Blacklist.findOne({ + token: req.user.accessToken, + }); // If the token is in the blacklist, return a 401 status if (tokenInBlacklist) { @@ -82,9 +83,10 @@ const isLoggedIn = async (req, res, next) => { next(); } catch (error) { // If there's an error while querying the database, return a 500 status with a detailed error message - return res - .status(500) - .json({ message: "Error retrieving token blacklist", error: error.message }); + return res.status(500).json({ + message: "Error retrieving token blacklist", + error: error.message, + }); } } else { // If the user is not logged in, return a 403 status diff --git a/backend/routes/adminRoutes.js b/backend/routes/adminRoutes.js index 80da7e7e4..4a0633336 100644 --- a/backend/routes/adminRoutes.js +++ b/backend/routes/adminRoutes.js @@ -1,10 +1,8 @@ import express from "express"; import User from "../model/user-model"; -//import Blacklist from "../model/blacklist-model" import bcrypt from "bcrypt"; import { authenticateUser, authorizeUser } from "../middleware/Middleware"; import dotenv from "dotenv"; -//import jwt from 'jsonwebtoken'; dotenv.config(); const SECRET = process.env.SECRET || "toast is the best secret"; @@ -63,14 +61,12 @@ adminRouter.put("/users/:id", async (req, res) => { const { name, email, role, password } = req.body; if (!(name || email || role || password)) { - return res - .status(400) - .json({ - error: "At least noe field is required to update user data", - error: error.message, - }); + return res.status(400).json({ + error: "At least noe field is required to update user data", + error: error.message, + }); } - + // make a new object to store the updated user data, if any field is updated let update = {}; if (name) update.name = name; if (email) update.email = email; diff --git a/backend/routes/routes.js b/backend/routes/routes.js index 681b9cf9e..a8fae43e9 100644 --- a/backend/routes/routes.js +++ b/backend/routes/routes.js @@ -94,16 +94,10 @@ router.post("/login", async (req, res) => { }); //route to verify if token is valid with middleware isLoggedIn -router.get("/verify", authenticateUser,(req, res) => { +router.get("/verify", authenticateUser, isLoggedIn, (req, res) => { res.json({ message: "You are logged in" }); }); - -//route to verify if token is valid with middleware isLoggedIn -//router.get("/verify", authenticateUser, isLoggedIn,(req, res) => { -// res.json({ message: "You are logged in" }); -//}); - //route to get user role router.get("/role", authenticateUser, (req, res) => { res.json({ role: req.user.role }); diff --git a/backend/server.js b/backend/server.js index 3a4f75ee3..b445f5aba 100644 --- a/backend/server.js +++ b/backend/server.js @@ -16,9 +16,7 @@ const app = express(); const allowedOrigins = [ "https://auntauthy.netlify.app", - "https://aunt-authy.onrender.com", - "http://localhost:5173", - "http://localhost:8787", + "https://aunt-authy.onrender.com" ]; app.use( diff --git a/frontend/components/Admin.jsx b/frontend/components/Admin.jsx index 482b3073a..9a0ff8049 100644 --- a/frontend/components/Admin.jsx +++ b/frontend/components/Admin.jsx @@ -6,12 +6,12 @@ import { DeleteUser } from './forms/DeleteUser'; import { CreateUser } from './forms/CreateUser'; import { UpdateUserRole } from './forms/UpdateUserRole'; - export const Admin = () => { const apiKey = import.meta.env.VITE_API_KEY; const API = apiKey + "/admin"; const [users, setUsers] = useState([]); const token = sessionStorage.getItem('token'); + const [message, setMessage] = useState(""); const getUsers = useCallback(async () => { try { @@ -24,8 +24,9 @@ export const Admin = () => { }); const data = await response.json(); setUsers(data); + setMessage('User fetched successfully'); } catch (error) { - console.error('Failed to fetch users:', error); + setMessage('An unexpected error occurred.'); } }, [API, token, setUsers]); @@ -41,12 +42,12 @@ export const Admin = () => {

As an admin can you do lots of exclusive stuff: create and delete users, update user roles, and userinfo

- - + + - + {users.map(({ _id, name, email, role }) => (
@@ -58,6 +59,7 @@ export const Admin = () => {
))} + {message &&

{message}

}
); } diff --git a/frontend/components/Dashboard.jsx b/frontend/components/Dashboard.jsx index a594a30e8..5eecb2b88 100644 --- a/frontend/components/Dashboard.jsx +++ b/frontend/components/Dashboard.jsx @@ -7,7 +7,7 @@ export const Dashboard = () => { const navigate = useNavigate(); const apiKey = import.meta.env.VITE_API_KEY; const API = apiKey + "/role"; - + useEffect(() => { const fetchData = async () => { @@ -40,7 +40,7 @@ export const Dashboard = () => { return ( <>

Dashboard

-

This is a page you have to be logged into see!

+

This is a page you have to be logged into see!

{isAdmin ? ( diff --git a/frontend/components/Home.jsx b/frontend/components/Home.jsx index 896d78453..09b6acedf 100644 --- a/frontend/components/Home.jsx +++ b/frontend/components/Home.jsx @@ -6,8 +6,8 @@ export const Home = () => { const [showLogin, setShowLogin] = useState(true); return ( <> -

Home

+

Welcome home old or new sailors!

{showLogin ? ( <>

Not a member? Register here

diff --git a/frontend/components/Login.jsx b/frontend/components/Login.jsx index 1f62c230e..c8a662ab8 100644 --- a/frontend/components/Login.jsx +++ b/frontend/components/Login.jsx @@ -1,5 +1,4 @@ import { useState } from "react"; -import { Logout } from "./Logout.jsx"; import { useNavigate } from "react-router-dom"; export const Login = () => { @@ -10,7 +9,6 @@ export const Login = () => { const apiKey = import.meta.env.VITE_API_KEY; const API = apiKey + "/login"; - const handleLogin = (event) => { event.preventDefault(); fetch(API, { @@ -63,7 +61,6 @@ export const Login = () => { {message && (

{message}

-
)} diff --git a/frontend/components/Logout.jsx b/frontend/components/Logout.jsx index db3c15916..51498d4f9 100644 --- a/frontend/components/Logout.jsx +++ b/frontend/components/Logout.jsx @@ -12,15 +12,13 @@ export const Logout = () => { try { // Retrieve the token from session storage const yourToken = sessionStorage.getItem('token'); - const response = await fetch(API, { + await fetch(API, { method: "POST", headers: { "Content-Type": "application/json", "Authorization": `Bearer ${yourToken}` }, }); - //const data = await response.json(); - //console.log(data); // Let's Kon-Mari that token who does not spark joy anymore. it has no purpose and is no longer worthy of our neat session storage sessionStorage.removeItem('token'); setMessage("Logout successful! Please sign in."); diff --git a/frontend/components/Registration.jsx b/frontend/components/Registration.jsx index 7ac9cabc1..19dcedb16 100644 --- a/frontend/components/Registration.jsx +++ b/frontend/components/Registration.jsx @@ -39,8 +39,7 @@ export const Registration = () => { } return res.json(); }) - .then((json) => { - console.log(json); + .then(() => { setMessage("Registration successful! Please sign in."); navigate("/login"); }) diff --git a/frontend/components/forms/CreateUser.jsx b/frontend/components/forms/CreateUser.jsx index 6e24a6927..7d8405b9b 100644 --- a/frontend/components/forms/CreateUser.jsx +++ b/frontend/components/forms/CreateUser.jsx @@ -3,7 +3,7 @@ import { useState } from 'react'; const apiKey = import.meta.env.VITE_API_KEY; const API = apiKey + "/admin" -export const CreateUser = ({getUsers}) => { +export const CreateUser = ({ getUsers }) => { const [name, setName] = useState(''); const [email, setEmail] = useState(''); const [role, setRole] = useState('user'); @@ -25,11 +25,8 @@ export const CreateUser = ({getUsers}) => { setMessage('An error occurred while creating the user.'); const errorData = await response.json(); throw new Error(errorData.error); - - } - //const data = await response.json(); - //console.log(data); + getUsers(); setMessage('User created successfully'); } catch (error) { diff --git a/frontend/components/forms/DeleteUser.jsx b/frontend/components/forms/DeleteUser.jsx index 7e3f4ba2d..faa99f813 100644 --- a/frontend/components/forms/DeleteUser.jsx +++ b/frontend/components/forms/DeleteUser.jsx @@ -1,6 +1,6 @@ import { useState } from "react"; -export const DeleteUser = ({getUsers}) => { +export const DeleteUser = ({ getUsers }) => { const apiKey = import.meta.env.VITE_API_KEY; const API = apiKey + "/admin"; const token = sessionStorage.getItem('token'); @@ -10,15 +10,14 @@ export const DeleteUser = ({getUsers}) => { e.preventDefault(); const id = e.target.id.value; try { - const response = await fetch(`${API}/users/${id}`, { + await fetch(`${API}/users/${id}`, { method: 'DELETE', headers: { 'Content-Type': 'application/json', 'Authorization': `Bearer ${token}` }, }); - //const data = await response.json(); - //console.log(data); + setMessage('User deleted successfully'); getUsers(); } diff --git a/frontend/components/forms/UpdateUser.jsx b/frontend/components/forms/UpdateUser.jsx index 61f0928d7..916ba8bca 100644 --- a/frontend/components/forms/UpdateUser.jsx +++ b/frontend/components/forms/UpdateUser.jsx @@ -1,6 +1,5 @@ import { useState, useEffect } from 'react'; - const apiKey = import.meta.env.VITE_API_KEY; const API = apiKey + "/admin" @@ -52,10 +51,10 @@ export const UpdateUser = ({ getUsers }) => { }); if (response.ok) { setMessage("User updated successfully"); -getUsers(); + getUsers(); } else if (!response.ok) { - setErrors("An error occurred while updating the user." + response.statusText); + setMessage("An error occurred while updating the user." + response.statusText); } } catch (error) { From ff89a782a1630e48350a2bac82f51bd0ff6e4958 Mon Sep 17 00:00:00 2001 From: Kathinka Sewell <4355167+kathinka@users.noreply.github.com> Date: Thu, 30 May 2024 18:48:49 +0200 Subject: [PATCH 27/41] fixed bad naming convention --- backend/model/user-model.js | 1 + backend/routes/routes.js | 2 +- backend/server.js | 4 ++-- frontend/README.md | 22 +++++++++++++++++----- frontend/_redirects | 2 +- frontend/components/Login.jsx | 2 +- 6 files changed, 23 insertions(+), 10 deletions(-) diff --git a/backend/model/user-model.js b/backend/model/user-model.js index f9526ff54..211fe74f9 100644 --- a/backend/model/user-model.js +++ b/backend/model/user-model.js @@ -13,6 +13,7 @@ const userSchema = new mongoose.Schema({ type: String, required: [true, "Email is required"], unique: true, + match: [/\S+@\S+\.\S+/, 'Email is invalid'], }, password: { type: String, diff --git a/backend/routes/routes.js b/backend/routes/routes.js index a8fae43e9..ee4eda05a 100644 --- a/backend/routes/routes.js +++ b/backend/routes/routes.js @@ -67,7 +67,7 @@ router.post("/signup", async (req, res) => { }); //log in user -router.post("/login", async (req, res) => { +router.post("/session", async (req, res) => { try { const { email, password } = req.body; const user = await User.findOne({ email }); diff --git a/backend/server.js b/backend/server.js index b445f5aba..0f9e7825a 100644 --- a/backend/server.js +++ b/backend/server.js @@ -16,7 +16,7 @@ const app = express(); const allowedOrigins = [ "https://auntauthy.netlify.app", - "https://aunt-authy.onrender.com" + "https://aunt-authy.onrender.com", ]; app.use( @@ -55,7 +55,7 @@ app.get("/", (req, res) => { "This route checks if a user with the provided email already exists in the database. It expects a JSON body with an 'email' field.", "/signup": "This route creates a new user with the provided name, email, and password. The password is hashed before being stored in the database. Upon successful creation, it returns the new user's ID and a JWT access token.", - "/login": + "/session": "This route logs in a user with the provided email and password. If the email and password match a user in the database, it returns the user's ID, a new JWT access token, and the user's role.", "/verify": "This route verifies if a user is authenticated by checking their JWT access token. and if their token is not stored in our blacklist. If the token is valid, it returns a message saying the user is logged in.", diff --git a/frontend/README.md b/frontend/README.md index f768e33fc..b78944a9c 100644 --- a/frontend/README.md +++ b/frontend/README.md @@ -1,8 +1,20 @@ -# React + Vite +# Project Auth -This template provides a minimal setup to get React working in Vite with HMR and some ESLint rules. +This is the auth project for week 16. + +A simple solution for testing out authorization, authentication and token blacklists. + +a website where you can make a user, log in to a dash and log out. +if you are assigned a role as admin, you will se an login for cool admins in dash where you can go to do important admin stuff, such ass creating new users, adjusting data for users, assing roles and delete users. .. and log out + + + + Auth Aunty + API + +test admin : admin@admin.com / pass: admin +test user : user@user.com / pass: user + +or make your own users if you like. admin has a separate login with more options, but easy to miss because the frontend got no love at all -Currently, two official plugins are available: -- [@vitejs/plugin-react](https://github.com/vitejs/vite-plugin-react/blob/main/packages/plugin-react/README.md) uses [Babel](https://babeljs.io/) for Fast Refresh -- [@vitejs/plugin-react-swc](https://github.com/vitejs/vite-plugin-react-swc) uses [SWC](https://swc.rs/) for Fast Refresh diff --git a/frontend/_redirects b/frontend/_redirects index cd12647f1..f2af938bf 100644 --- a/frontend/_redirects +++ b/frontend/_redirects @@ -1,6 +1,6 @@ /exists https://project-auth-pqxu.onrender.com/exists 200 /signup https://project-auth-pqxu.onrender.com/signup 200 -/login https://project-auth-pqxu.onrender.com/login 200 +/session https://project-auth-pqxu.onrender.com/session 200 /verify https://project-auth-pqxu.onrender.com/verify 200 /role https://project-auth-pqxu.onrender.com/role 200 /logout https://project-auth-pqxu.onrender.com/logout 200 diff --git a/frontend/components/Login.jsx b/frontend/components/Login.jsx index c8a662ab8..4c6487673 100644 --- a/frontend/components/Login.jsx +++ b/frontend/components/Login.jsx @@ -7,7 +7,7 @@ export const Login = () => { const [message, setMessage] = useState(""); const navigate = useNavigate(); const apiKey = import.meta.env.VITE_API_KEY; - const API = apiKey + "/login"; + const API = apiKey + "/session"; const handleLogin = (event) => { event.preventDefault(); From c2f1a5cfb7b3c807df45c1c059dfc526687bffb1 Mon Sep 17 00:00:00 2001 From: Kathinka Sewell <4355167+kathinka@users.noreply.github.com> Date: Thu, 30 May 2024 18:56:40 +0200 Subject: [PATCH 28/41] added a wildcard in my routes to handle routes not specified - aka page not found --- frontend/routes/AppRoutes.jsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/frontend/routes/AppRoutes.jsx b/frontend/routes/AppRoutes.jsx index cf13c7047..2e455d298 100644 --- a/frontend/routes/AppRoutes.jsx +++ b/frontend/routes/AppRoutes.jsx @@ -18,7 +18,7 @@ export const AppRoutes = () => { } /> } fallbackComponent={} /> } /> - + Not Found} /> From 0f500dbe33819c9130dcd7ed618e9b8b7204415f Mon Sep 17 00:00:00 2001 From: Kathinka Sewell <4355167+kathinka@users.noreply.github.com> Date: Thu, 30 May 2024 21:17:56 +0200 Subject: [PATCH 29/41] chore: Update signup route to use "/user" instead of "/signup" to remove bad naming conventions. also improved my user model to hash the password before storing it in the db, so it will be a lot more secure. also removed storing of the accesstoken in the db, and made room for upgrading with a refresh token instead. (in the future) --- backend/model/user-model.js | 29 ++++++++++++++------ backend/routes/adminRoutes.js | 24 +++++++--------- backend/routes/routes.js | 41 +++++++++++++--------------- backend/server.js | 6 ++-- frontend/_redirects | 2 +- frontend/components/Registration.jsx | 2 +- 6 files changed, 56 insertions(+), 48 deletions(-) diff --git a/backend/model/user-model.js b/backend/model/user-model.js index 211fe74f9..10b9322e6 100644 --- a/backend/model/user-model.js +++ b/backend/model/user-model.js @@ -1,6 +1,9 @@ import mongoose from "mongoose"; import bcrypt from "bcrypt"; + +const SALT_ROUNDS = 12; // make this configurable so we can adjust the security level if needed + // Create a schema const userSchema = new mongoose.Schema({ name: { @@ -25,16 +28,26 @@ const userSchema = new mongoose.Schema({ enum: ["user", "writer", "editor", "admin"], default: "user", }, - accessToken: { + refreshToken: { type: String, - default: () => { - return bcrypt.hashSync(Math.random().toString(36).substring(2), 10); - }, + default: null, }, }); -// Create a model -const User = mongoose.model("User", userSchema); +//use matchPassword method to compare the entered password with the hashed password in the database +userSchema.methods.matchPassword = async function(enteredPassword) { + return await bcrypt.compare(enteredPassword, this.password); +} +//add a pre-save hook to hash the password *before* saving it to the database so we don't store the password in plain text because we are mysterious and security conscious +userSchema.pre('save', async function(next) { + // we only hash the password if it has been modified (or is new) + if (!this.isModified('password')) { + next(); + } + // hash the password before saving it to the database with the SALT_ROUNDS we can easoly adjust the security level. + this.password = await bcrypt.hash(this.password, SALT_ROUNDS); +}); + + -// Export the model -export default User; +export default mongoose.model('User', userSchema); diff --git a/backend/routes/adminRoutes.js b/backend/routes/adminRoutes.js index 4a0633336..288c088a7 100644 --- a/backend/routes/adminRoutes.js +++ b/backend/routes/adminRoutes.js @@ -62,26 +62,22 @@ adminRouter.put("/users/:id", async (req, res) => { if (!(name || email || role || password)) { return res.status(400).json({ - error: "At least noe field is required to update user data", - error: error.message, + error: "At least one field is required to update user data", }); } - // make a new object to store the updated user data, if any field is updated - let update = {}; - if (name) update.name = name; - if (email) update.email = email; - if (role) update.role = role; - if (password) { - const salt = bcrypt.genSaltSync(); - update.password = bcrypt.hashSync(password, salt); - } - - const updatedUser = await User.findByIdAndUpdate(id, update, { new: true }); - if (!updatedUser) { + const user = await User.findById(id); + if (!user) { return res.status(404).json({ error: "User not found" }); } + if (name) user.name = name; + if (email) user.email = email; + if (role) user.role = role; + if (password) user.password = password; + + const updatedUser = await user.save(); + res.json(updatedUser); } catch (error) { console.error(error); diff --git a/backend/routes/routes.js b/backend/routes/routes.js index ee4eda05a..de070794d 100644 --- a/backend/routes/routes.js +++ b/backend/routes/routes.js @@ -1,6 +1,5 @@ import express from "express"; import User from "../model/user-model"; -import bcrypt from "bcrypt"; import { authenticateUser, logoutUser, @@ -38,26 +37,23 @@ router.post("/exists", async (req, res) => { }); // add user -router.post("/signup", async (req, res) => { +router.post("/user", async (req, res) => { const { name, email, password } = req.body; try { - const salt = bcrypt.genSaltSync(); const newUser = await new User({ name, email, - password: bcrypt.hashSync(password, salt), + password, }).save(); // Generate a new access token const newAccessToken = jwt.sign({ id: newUser._id }, SECRET, { expiresIn: "1h", }); - // Update the new user's access token - newUser.accessToken = newAccessToken; res.status(201).json({ userId: newUser._id, - accessToken: newUser.accessToken, + accessToken: newAccessToken, }); } catch (err) { res @@ -71,7 +67,7 @@ router.post("/session", async (req, res) => { try { const { email, password } = req.body; const user = await User.findOne({ email }); - if (user && bcrypt.compareSync(password, user.password)) { + if (user && (await user.matchPassword(password))) { //generate the access token const newAccessToken = jwt.sign({ id: user._id }, SECRET, { expiresIn: "1h", @@ -112,20 +108,21 @@ router.post("/logout", authenticateUser, logoutUser, (req, res) => { router.patch("/users/:id", async (req, res) => { const { id } = req.params; const { name, email, password } = req.body; - const salt = bcrypt.genSaltSync(); - const updatedUser = await User.findByIdAndUpdate( - id, - { - name, - email, - password: bcrypt.hashSync(password, salt), - }, - { new: true } - ); - if (updatedUser) { - res.json(updatedUser); - } else { - res.status(404).json({ message: "User not found", error: error.message }); + try { + const user = await User.findById(id); + if (user) { + user.name = name; + user.email = email; + user.password = password; + const updatedUser = await user.save(); + res.json(updatedUser); + } else { + res.status(404).json({ message: "User not found" }); + } + } catch (error) { + res + .status(500) + .json({ message: "An error occurred", error: error.message }); } }); diff --git a/backend/server.js b/backend/server.js index 0f9e7825a..c2b53bd56 100644 --- a/backend/server.js +++ b/backend/server.js @@ -7,7 +7,7 @@ import dotenv from "dotenv"; import listEndpoints from "express-list-endpoints"; dotenv.config(); -const mongoUrl = process.env.MONGO_URL || "mongodb://localhost/authAPI"; +const mongoUrl = process.env.MONGO_URL || "mongodb://localhost/aunty"; mongoose.connect(mongoUrl); mongoose.Promise = Promise; @@ -16,7 +16,9 @@ const app = express(); const allowedOrigins = [ "https://auntauthy.netlify.app", + "http://localhost:8787", "https://aunt-authy.onrender.com", + "http://localhost:5174", ]; app.use( @@ -53,7 +55,7 @@ app.get("/", (req, res) => { "This route renders the admin page. It requires admin authorization.", "/exists": "This route checks if a user with the provided email already exists in the database. It expects a JSON body with an 'email' field.", - "/signup": + "/user": "This route creates a new user with the provided name, email, and password. The password is hashed before being stored in the database. Upon successful creation, it returns the new user's ID and a JWT access token.", "/session": "This route logs in a user with the provided email and password. If the email and password match a user in the database, it returns the user's ID, a new JWT access token, and the user's role.", diff --git a/frontend/_redirects b/frontend/_redirects index f2af938bf..e126d85c8 100644 --- a/frontend/_redirects +++ b/frontend/_redirects @@ -1,5 +1,5 @@ /exists https://project-auth-pqxu.onrender.com/exists 200 -/signup https://project-auth-pqxu.onrender.com/signup 200 +/user https://project-auth-pqxu.onrender.com/user 200 /session https://project-auth-pqxu.onrender.com/session 200 /verify https://project-auth-pqxu.onrender.com/verify 200 /role https://project-auth-pqxu.onrender.com/role 200 diff --git a/frontend/components/Registration.jsx b/frontend/components/Registration.jsx index 19dcedb16..8fb44bb96 100644 --- a/frontend/components/Registration.jsx +++ b/frontend/components/Registration.jsx @@ -8,7 +8,7 @@ export const Registration = () => { const [message, setMessage] = useState(""); const navigate = useNavigate(); const apiKey = import.meta.env.VITE_API_KEY; - const signup = apiKey + "/signup"; + const signup = apiKey + "/user"; const exist = apiKey + "/exists"; const handleRegistration = (event) => { From 9077b0a861b371a3128a4fbe3c376fc52313b816 Mon Sep 17 00:00:00 2001 From: Kathinka Sewell <4355167+kathinka@users.noreply.github.com> Date: Thu, 30 May 2024 21:24:53 +0200 Subject: [PATCH 30/41] Update user model to hash passwords and remove storing of access tokens --- backend/model/user-model.js | 1 + backend/server.js | 5 ++--- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/backend/model/user-model.js b/backend/model/user-model.js index 10b9322e6..9f276a557 100644 --- a/backend/model/user-model.js +++ b/backend/model/user-model.js @@ -34,6 +34,7 @@ const userSchema = new mongoose.Schema({ }, }); + //use matchPassword method to compare the entered password with the hashed password in the database userSchema.methods.matchPassword = async function(enteredPassword) { return await bcrypt.compare(enteredPassword, this.password); diff --git a/backend/server.js b/backend/server.js index c2b53bd56..a975505b7 100644 --- a/backend/server.js +++ b/backend/server.js @@ -16,9 +16,8 @@ const app = express(); const allowedOrigins = [ "https://auntauthy.netlify.app", - "http://localhost:8787", - "https://aunt-authy.onrender.com", - "http://localhost:5174", + "https://aunt-authy.onrender.com" + ]; app.use( From 83767022d192e27f9b4e4ec4c47c584af8bc4d36 Mon Sep 17 00:00:00 2001 From: Kathinka Sewell <4355167+kathinka@users.noreply.github.com> Date: Thu, 30 May 2024 21:37:23 +0200 Subject: [PATCH 31/41] Update user model to use bcryptjs for password hashing instad of bcrypt as its no longer supported --- backend/model/user-model.js | 22 +++++++++------------- backend/package.json | 4 ++-- backend/routes/adminRoutes.js | 6 +++--- package.json | 1 - 4 files changed, 14 insertions(+), 19 deletions(-) diff --git a/backend/model/user-model.js b/backend/model/user-model.js index 9f276a557..c39407a5e 100644 --- a/backend/model/user-model.js +++ b/backend/model/user-model.js @@ -1,6 +1,5 @@ import mongoose from "mongoose"; -import bcrypt from "bcrypt"; - +import bcryptjs from "bcryptjs"; const SALT_ROUNDS = 12; // make this configurable so we can adjust the security level if needed @@ -16,7 +15,7 @@ const userSchema = new mongoose.Schema({ type: String, required: [true, "Email is required"], unique: true, - match: [/\S+@\S+\.\S+/, 'Email is invalid'], + match: [/\S+@\S+\.\S+/, "Email is invalid"], }, password: { type: String, @@ -34,21 +33,18 @@ const userSchema = new mongoose.Schema({ }, }); - //use matchPassword method to compare the entered password with the hashed password in the database -userSchema.methods.matchPassword = async function(enteredPassword) { - return await bcrypt.compare(enteredPassword, this.password); -} +userSchema.methods.matchPassword = async function (enteredPassword) { + return await bcryptjs.compare(enteredPassword, this.password); +}; //add a pre-save hook to hash the password *before* saving it to the database so we don't store the password in plain text because we are mysterious and security conscious -userSchema.pre('save', async function(next) { +userSchema.pre("save", async function (next) { // we only hash the password if it has been modified (or is new) - if (!this.isModified('password')) { + if (!this.isModified("password")) { next(); } // hash the password before saving it to the database with the SALT_ROUNDS we can easoly adjust the security level. - this.password = await bcrypt.hash(this.password, SALT_ROUNDS); + this.password = await bcryptjs.hash(this.password, SALT_ROUNDS); }); - - -export default mongoose.model('User', userSchema); +export default mongoose.model("User", userSchema); diff --git a/backend/package.json b/backend/package.json index 2e98b3eef..93a0cf50b 100644 --- a/backend/package.json +++ b/backend/package.json @@ -1,7 +1,7 @@ { "name": "project-auth-backend", "version": "1.0.0", - "description": "Project Aunt Authy - a test project for authentication - authentication with JWT, bcrypt, express, mongoose, and one strict Aunt Authy", + "description": "Project Aunt Authy - a test project for authentication - authentication with JWT, bcryptjs, express, mongoose, and one strict Aunt Authy", "scripts": { "start": "babel-node server.js", "dev": "nodemon server.js --exec babel-node" @@ -27,4 +27,4 @@ "mongoose": "^8.0.0", "nodemon": "^3.0.1" } -} +} \ No newline at end of file diff --git a/backend/routes/adminRoutes.js b/backend/routes/adminRoutes.js index 288c088a7..6d3533b4a 100644 --- a/backend/routes/adminRoutes.js +++ b/backend/routes/adminRoutes.js @@ -1,6 +1,6 @@ import express from "express"; import User from "../model/user-model"; -import bcrypt from "bcrypt"; +import bcryptjs from "bcryptjs"; import { authenticateUser, authorizeUser } from "../middleware/Middleware"; import dotenv from "dotenv"; @@ -37,12 +37,12 @@ adminRouter.post("/users", async (req, res) => { .status(400) .json({ error: "All fields are required", error: error.message }); } - const salt = bcrypt.genSaltSync(); + const salt = bcryptjs.genSaltSync(); const newUser = new User({ name, email, role, - password: bcrypt.hashSync(password, salt), + password: bcryptjs.hashSync(password, salt), }); const savedUser = await newUser.save(); res.json(savedUser); diff --git a/package.json b/package.json index ddc0008b0..151a07404 100644 --- a/package.json +++ b/package.json @@ -5,7 +5,6 @@ "postinstall": "npm install --prefix backend" }, "dependencies": { - "bcrypt": "^5.1.1", "bcryptjs": "^2.4.3" } } From 25934a9d474d5e1023aadb99f3e45cc754ae4977 Mon Sep 17 00:00:00 2001 From: Kathinka Sewell <4355167+kathinka@users.noreply.github.com> Date: Thu, 30 May 2024 23:00:45 +0200 Subject: [PATCH 32/41] removed unneccessary packages, added checks in post new users endpoint to check for duplicates. --- backend/package.json | 5 ++--- backend/routes/adminRoutes.js | 10 +++++++--- backend/routes/routes.js | 7 +++++++ frontend/components/Logout.jsx | 3 +-- frontend/package.json | 1 - package.json | 3 --- 6 files changed, 17 insertions(+), 12 deletions(-) diff --git a/backend/package.json b/backend/package.json index 93a0cf50b..bbb9e2662 100644 --- a/backend/package.json +++ b/backend/package.json @@ -13,7 +13,7 @@ "@babel/node": "^7.16.8", "@babel/preset-env": "^7.16.11", "atob": "^2.1.2", - "bcrypt": "^5.1.1", + "bcryptjs": "^2.4.3", "cookie-parser": "^1.4.6", "cors": "^2.8.5", "csurf": "^1.11.0", @@ -21,10 +21,9 @@ "dotenv": "^16.4.5", "express": "^4.17.3", "express-list-endpoints": "^7.1.0", - "express-list-routes": "^1.2.1", "express-session": "^1.18.0", "jsonwebtoken": "^9.0.2", "mongoose": "^8.0.0", "nodemon": "^3.0.1" } -} \ No newline at end of file +} diff --git a/backend/routes/adminRoutes.js b/backend/routes/adminRoutes.js index 6d3533b4a..2ffc1686a 100644 --- a/backend/routes/adminRoutes.js +++ b/backend/routes/adminRoutes.js @@ -1,6 +1,5 @@ import express from "express"; import User from "../model/user-model"; -import bcryptjs from "bcryptjs"; import { authenticateUser, authorizeUser } from "../middleware/Middleware"; import dotenv from "dotenv"; @@ -37,12 +36,17 @@ adminRouter.post("/users", async (req, res) => { .status(400) .json({ error: "All fields are required", error: error.message }); } - const salt = bcryptjs.genSaltSync(); + const existingUser = await User.findOne({ email: { email } }); + if (existingUser) { + return res + .status(400) + .send({ message: "User with this email already exists" }); + } const newUser = new User({ name, email, role, - password: bcryptjs.hashSync(password, salt), + password, }); const savedUser = await newUser.save(); res.json(savedUser); diff --git a/backend/routes/routes.js b/backend/routes/routes.js index de070794d..5c393d9e0 100644 --- a/backend/routes/routes.js +++ b/backend/routes/routes.js @@ -40,6 +40,13 @@ router.post("/exists", async (req, res) => { router.post("/user", async (req, res) => { const { name, email, password } = req.body; try { + // check for duplicate user, and ensure that the input is treated as a literal value ($eq) to protect against NoSQL injection + const existingUser = await User.findOne({ email: { $eq: email } }); + if (existingUser) { + return res + .status(400) + .send({ message: "User with this email already exists" }); + } const newUser = await new User({ name, email, diff --git a/frontend/components/Logout.jsx b/frontend/components/Logout.jsx index 51498d4f9..2a4d462b2 100644 --- a/frontend/components/Logout.jsx +++ b/frontend/components/Logout.jsx @@ -31,9 +31,8 @@ export const Logout = () => { return (
-

Logout

-

{message}

+

{message}

); } diff --git a/frontend/package.json b/frontend/package.json index 7a75bf20d..5f778fc94 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -12,7 +12,6 @@ "dependencies": { "dompurify": "^3.1.4", "dotenv": "^16.4.5", - "jwt-decode": "^3.1.2", "react": "^18.2.0", "react-dom": "^18.2.0", "react-router-dom": "^6.23.1" diff --git a/package.json b/package.json index 151a07404..d774b8cc3 100644 --- a/package.json +++ b/package.json @@ -3,8 +3,5 @@ "version": "1.0.0", "scripts": { "postinstall": "npm install --prefix backend" - }, - "dependencies": { - "bcryptjs": "^2.4.3" } } From b8f6a0d750b3ab9ce5470cd5572f65e8ac85d2e1 Mon Sep 17 00:00:00 2001 From: Kathinka Sewell <4355167+kathinka@users.noreply.github.com> Date: Thu, 30 May 2024 23:07:07 +0200 Subject: [PATCH 33/41] Refactor user routes to use existingUser variable consistently --- backend/routes/adminRoutes.js | 2 +- backend/routes/routes.js | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/backend/routes/adminRoutes.js b/backend/routes/adminRoutes.js index 2ffc1686a..323833201 100644 --- a/backend/routes/adminRoutes.js +++ b/backend/routes/adminRoutes.js @@ -36,7 +36,7 @@ adminRouter.post("/users", async (req, res) => { .status(400) .json({ error: "All fields are required", error: error.message }); } - const existingUser = await User.findOne({ email: { email } }); + const existingUser = await User.findOne({ email }); if (existingUser) { return res .status(400) diff --git a/backend/routes/routes.js b/backend/routes/routes.js index 5c393d9e0..4300039f2 100644 --- a/backend/routes/routes.js +++ b/backend/routes/routes.js @@ -16,8 +16,8 @@ const router = express.Router(); router.post("/exists", async (req, res) => { const { email } = req.body; try { - const user = await User.findOne({ email: email }); - if (user) { + const existingUser = await User.findOne({ email }); + if (existingUser) { res.status(400).json({ exists: true, message: "User already exists", From 57a0b5e261bdaaf3e2fd3caf83b518305b38e484 Mon Sep 17 00:00:00 2001 From: Kathinka Sewell <4355167+kathinka@users.noreply.github.com> Date: Thu, 30 May 2024 23:17:14 +0200 Subject: [PATCH 34/41] improve error handling on user creation --- backend/routes/adminRoutes.js | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/backend/routes/adminRoutes.js b/backend/routes/adminRoutes.js index 323833201..ef7799d3c 100644 --- a/backend/routes/adminRoutes.js +++ b/backend/routes/adminRoutes.js @@ -32,9 +32,7 @@ adminRouter.post("/users", async (req, res) => { try { const { name, email, role, password } = req.body; if (!name || !email || !role || !password) { - return res - .status(400) - .json({ error: "All fields are required", error: error.message }); + return res.status(400).json({ error: "All fields are required" }); } const existingUser = await User.findOne({ email }); if (existingUser) { From 0a96680f733b9160d443b23edbdaeb8076f37d35 Mon Sep 17 00:00:00 2001 From: Kathinka Sewell <4355167+kathinka@users.noreply.github.com> Date: Thu, 30 May 2024 23:17:42 +0200 Subject: [PATCH 35/41] Improve error handling on user creation --- backend/routes/adminRoutes.js | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/backend/routes/adminRoutes.js b/backend/routes/adminRoutes.js index ef7799d3c..88bf85951 100644 --- a/backend/routes/adminRoutes.js +++ b/backend/routes/adminRoutes.js @@ -50,8 +50,7 @@ adminRouter.post("/users", async (req, res) => { res.json(savedUser); } catch (error) { res.status(500).json({ - error: "An error occurred while adding the user", - error: error.message, + error: "An error occurred while adding the user" }); } }); From abf9575ae158d9c1f9ffc3cf0cdb24b811a1cf22 Mon Sep 17 00:00:00 2001 From: Kathinka Sewell <4355167+kathinka@users.noreply.github.com> Date: Thu, 30 May 2024 23:51:07 +0200 Subject: [PATCH 36/41] Improve error handling on user creation --- frontend/components/forms/CreateUser.jsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/frontend/components/forms/CreateUser.jsx b/frontend/components/forms/CreateUser.jsx index 7d8405b9b..bc54f64cb 100644 --- a/frontend/components/forms/CreateUser.jsx +++ b/frontend/components/forms/CreateUser.jsx @@ -34,7 +34,7 @@ export const CreateUser = ({ getUsers }) => { if (error.message.includes('E11000')) { setMessage('A user with the same email already exists.'); } else { - setMessage('An unexpected error occurred.'); + setMessage(`An unexpected error occurred: ${error.message}`); } } }; From eb614f0f65eea926d5004fe5c95da5b1f30f5e8d Mon Sep 17 00:00:00 2001 From: Kathinka Sewell <4355167+kathinka@users.noreply.github.com> Date: Thu, 30 May 2024 23:55:09 +0200 Subject: [PATCH 37/41] added logging -Improve error handling on user creation --- frontend/components/forms/CreateUser.jsx | 1 + 1 file changed, 1 insertion(+) diff --git a/frontend/components/forms/CreateUser.jsx b/frontend/components/forms/CreateUser.jsx index bc54f64cb..418a9b2f9 100644 --- a/frontend/components/forms/CreateUser.jsx +++ b/frontend/components/forms/CreateUser.jsx @@ -34,6 +34,7 @@ export const CreateUser = ({ getUsers }) => { if (error.message.includes('E11000')) { setMessage('A user with the same email already exists.'); } else { + console.log(error); setMessage(`An unexpected error occurred: ${error.message}`); } } From 2fa5736b1f27d1225f95b51a10fbf0bdcb42223b Mon Sep 17 00:00:00 2001 From: Kathinka Sewell <4355167+kathinka@users.noreply.github.com> Date: Fri, 31 May 2024 00:03:41 +0200 Subject: [PATCH 38/41] Improve error handling on user creation and add logging ... --- frontend/components/forms/CreateUser.jsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/frontend/components/forms/CreateUser.jsx b/frontend/components/forms/CreateUser.jsx index 418a9b2f9..a572463eb 100644 --- a/frontend/components/forms/CreateUser.jsx +++ b/frontend/components/forms/CreateUser.jsx @@ -22,8 +22,8 @@ export const CreateUser = ({ getUsers }) => { body: JSON.stringify({ name, email, role, password }), }); if (!response.ok) { - setMessage('An error occurred while creating the user.'); const errorData = await response.json(); + setMessage(errorData.message || 'An error occurred while creating the user.'); throw new Error(errorData.error); } From a9155eaeb3666afe39e8976bb8102288a3eed1dc Mon Sep 17 00:00:00 2001 From: Kathinka Sewell <4355167+kathinka@users.noreply.github.com> Date: Fri, 31 May 2024 00:09:16 +0200 Subject: [PATCH 39/41] Improve error handling on user creation and remove logging --- frontend/components/forms/CreateUser.jsx | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/frontend/components/forms/CreateUser.jsx b/frontend/components/forms/CreateUser.jsx index a572463eb..f6ce348b9 100644 --- a/frontend/components/forms/CreateUser.jsx +++ b/frontend/components/forms/CreateUser.jsx @@ -30,11 +30,10 @@ export const CreateUser = ({ getUsers }) => { getUsers(); setMessage('User created successfully'); } catch (error) { - console.error(error); - if (error.message.includes('E11000')) { + // check if it includes the error message from the backend + if (error.message.includes('User with this email already exists')) { setMessage('A user with the same email already exists.'); } else { - console.log(error); setMessage(`An unexpected error occurred: ${error.message}`); } } From 9f9185dc1e8dcd4264fc1c7ece54c0b6a5e4b5ac Mon Sep 17 00:00:00 2001 From: Kathinka Sewell <4355167+kathinka@users.noreply.github.com> Date: Fri, 31 May 2024 00:13:16 +0200 Subject: [PATCH 40/41] Improve error handling on user creation --- frontend/components/forms/CreateUser.jsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/frontend/components/forms/CreateUser.jsx b/frontend/components/forms/CreateUser.jsx index f6ce348b9..3727a829c 100644 --- a/frontend/components/forms/CreateUser.jsx +++ b/frontend/components/forms/CreateUser.jsx @@ -24,7 +24,7 @@ export const CreateUser = ({ getUsers }) => { if (!response.ok) { const errorData = await response.json(); setMessage(errorData.message || 'An error occurred while creating the user.'); - throw new Error(errorData.error); + throw new Error(errorData.message || 'An error occurred while creating the user.'); } getUsers(); From 4a6f50b51976eb9b837d98feb293ae92def8618e Mon Sep 17 00:00:00 2001 From: Kathinka Sewell <4355167+kathinka@users.noreply.github.com> Date: Fri, 31 May 2024 00:18:32 +0200 Subject: [PATCH 41/41] trying to imrpove the error message handling --- frontend/components/forms/CreateUser.jsx | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/frontend/components/forms/CreateUser.jsx b/frontend/components/forms/CreateUser.jsx index 3727a829c..427ecd7f2 100644 --- a/frontend/components/forms/CreateUser.jsx +++ b/frontend/components/forms/CreateUser.jsx @@ -23,8 +23,9 @@ export const CreateUser = ({ getUsers }) => { }); if (!response.ok) { const errorData = await response.json(); - setMessage(errorData.message || 'An error occurred while creating the user.'); - throw new Error(errorData.message || 'An error occurred while creating the user.'); + const errorMessage = errorData.message || errorData.error; + setMessage(errorMessage || 'An error occurred while creating the user.'); + throw new Error(errorMessage); } getUsers();