Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
41 commits
Select commit Hold shift + click to select a range
b1f5151
Update backend dependencies and added some routes.
kathinka May 20, 2024
05fd5b1
chore: Update backend dependencies and add bcrypt package
kathinka May 20, 2024
3afddf2
Add admin routes for authentication and authorization
kathinka May 20, 2024
81ef732
Update user routes for signup and login
kathinka May 20, 2024
635c112
chore: Update frontend and backend dependencies
kathinka May 24, 2024
9e7bc3e
Remove the netlify.toml file that specified the build configuration f…
kathinka May 26, 2024
801a647
Update frontend and backend dependencies, remove deprecated options, …
kathinka May 26, 2024
37f8889
cleanup
kathinka May 26, 2024
96bf447
chore: Update CORS origin to include additional URLs and allow multip…
kathinka May 26, 2024
f17512f
build: Update build script to include _redirects file in dist folder
kathinka May 26, 2024
b106012
Update redirect URL for /role in _redirects file
kathinka May 27, 2024
d630105
chore: Update CORS origin to include additional URLs and allow multip…
kathinka May 27, 2024
049577e
chore: Update CORS origin to include additional URLs and allow multip…
kathinka May 27, 2024
d702362
Refactor Middleware.js to use ES6 module syntax
kathinka May 27, 2024
2019ed0
Update the fetch URL in the ProtectedRoutes.jsx file to use HTTPS ins…
kathinka May 27, 2024
eed23ef
Update fetch URL in ProtectedRoutes.jsx to use HTTPS for secure requests
kathinka May 27, 2024
3d3ad3d
chore: Update isLoggedIn middleware to asynchronously check token bla…
kathinka May 27, 2024
7b333fe
more logging
kathinka May 27, 2024
19d38d0
added handling of successful verification of token
kathinka May 27, 2024
5852942
adjust the token verification function
kathinka May 27, 2024
d6e836a
chore: Update CORS origin to allow all origins
kathinka May 27, 2024
9ac22af
Fix token blacklist check logic in isLoggedIn middleware
kathinka May 27, 2024
0507ee1
refactor Middleware.js to improve token handling and verification again
kathinka May 27, 2024
dc1cb89
fixed a bug with getting the accesstoken and verifying with thew blac…
kathinka May 27, 2024
9930dcd
improve code readability, added better user feedback, and more respo…
kathinka May 28, 2024
83c6a15
Improve code readability, add better user feedback, and make admin pa…
kathinka May 28, 2024
ff89a78
fixed bad naming convention
kathinka May 30, 2024
c2f1a5c
added a wildcard in my routes to handle routes not specified - aka pa…
kathinka May 30, 2024
0f500db
chore: Update signup route to use "/user" instead of "/signup" to rem…
kathinka May 30, 2024
9077b0a
Update user model to hash passwords and remove storing of access tokens
kathinka May 30, 2024
8376702
Update user model to use bcryptjs for password hashing instad of bcr…
kathinka May 30, 2024
25934a9
removed unneccessary packages, added checks in post new users endpoin…
kathinka May 30, 2024
b8f6a0d
Refactor user routes to use existingUser variable consistently
kathinka May 30, 2024
57a0b5e
improve error handling on user creation
kathinka May 30, 2024
0a96680
Improve error handling on user creation
kathinka May 30, 2024
abf9575
Improve error handling on user creation
kathinka May 30, 2024
eb614f0
added logging -Improve error handling on user creation
kathinka May 30, 2024
2fa5736
Improve error handling on user creation and add logging ...
kathinka May 30, 2024
a9155ea
Improve error handling on user creation and remove
kathinka May 30, 2024
9f9185d
Improve error handling on user creation
kathinka May 30, 2024
4a6f50b
trying to imrpove the error message handling
kathinka May 30, 2024
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions .env.example
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
VITE_API_KEY=
MONGO_URL=
SECRET=
2 changes: 2 additions & 0 deletions backend/.env.example
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
MONGO_URL=
SECRET=
122 changes: 122 additions & 0 deletions backend/middleware/Middleware.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,122 @@
import User from "../model/user-model";
import dotenv from "dotenv";
import Blacklist from "../model/blacklist-model";
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"];
//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 <token>'",
});
}
} else {
res.status(403).json({ loggedOut: true, message: "No token provided" });
}
};

// 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. Required roles: ${roles.join(
", "
)}`,
});
}
next();
};
};

//check if user is logged in and the token is not in the blacklist
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,
});

// If the token is in the blacklist, return a 401 status
if (tokenInBlacklist) {
return res
.status(401)
.json({ message: "This token has been invalidated" });
}

// 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(500).json({
message: "Error retrieving token blacklist",
error: error.message,
});
}
} else {
// If the user is not logged in, return a 403 status
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 });
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
res.status(500).json({
message: "An error occurred while logging out",
error: error.message,
});
}
};

export { authenticateUser, authorizeUser, logoutUser, isLoggedIn };
14 changes: 14 additions & 0 deletions backend/model/blacklist-model.js
Original file line number Diff line number Diff line change
@@ -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;
50 changes: 50 additions & 0 deletions backend/model/user-model.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
import mongoose from "mongoose";
import bcryptjs from "bcryptjs";

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: {
type: String,
required: [true, "Name is required"],
minlength: [2, "Name must be at least 2 characters"],
maxlength: [30, "Name must be at most 30 characters"],
},
email: {
type: String,
required: [true, "Email is required"],
unique: true,
match: [/\S+@\S+\.\S+/, "Email is invalid"],
},
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",
},
refreshToken: {
type: String,
default: null,
},
});

//use matchPassword method to compare the entered password with the hashed password in the database
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) {
// 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 bcryptjs.hash(this.password, SALT_ROUNDS);
});

export default mongoose.model("User", userSchema);
11 changes: 10 additions & 1 deletion backend/package.json
Original file line number Diff line number Diff line change
@@ -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, bcryptjs, express, mongoose, and one strict Aunt Authy",
"scripts": {
"start": "babel-node server.js",
"dev": "nodemon server.js --exec babel-node"
Expand All @@ -12,8 +12,17 @@
"@babel/core": "^7.17.9",
"@babel/node": "^7.16.8",
"@babel/preset-env": "^7.16.11",
"atob": "^2.1.2",
"bcryptjs": "^2.4.3",
"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-session": "^1.18.0",
"jsonwebtoken": "^9.0.2",
"mongoose": "^8.0.0",
"nodemon": "^3.0.1"
}
Expand Down
137 changes: 137 additions & 0 deletions backend/routes/adminRoutes.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,137 @@
import express from "express";
import User from "../model/user-model";
import { authenticateUser, authorizeUser } from "../middleware/Middleware";
import dotenv from "dotenv";

dotenv.config();
const SECRET = process.env.SECRET || "toast is the best 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();
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("/", (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" });
}
const existingUser = await User.findOne({ email });
if (existingUser) {
return res
.status(400)
.send({ message: "User with this email already exists" });
}
const newUser = new User({
name,
email,
role,
password,
});
const savedUser = await newUser.save();
res.json(savedUser);
} catch (error) {
res.status(500).json({
error: "An error occurred while adding the user"
});
}
});

//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: "At least one field is required to update user data",
});
}

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);
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;
Loading