diff --git a/README.md b/README.md index dfa05e177..b10ddf7f8 100644 --- a/README.md +++ b/README.md @@ -1,13 +1,15 @@ # Project Auth API -Replace this readme with your own information about your project. +A simple authentication API built with Node.js and Express. It allows users to sign up, log in, and access protected routes with a valid token. The frontend is made with Tailwind CSS. -Start by briefly describing the assignment in a sentence or two. Keep it short and to the point. ## The problem -Describe how you approached to problem, and what tools and techniques you used to solve it. How did you plan? What technologies did you use? If you had more time, what would be next? +I didnt understand the first codes that we were provided, I did everything a second time and used videos and lectures that I understood more of. If I had more time I would implement some sort of contact form in the /home directory and work more in the design. + ## View it live -Every project should be deployed somewhere. Be sure to include the link to the deployed project so that the viewer can click around and see what it's all about. +Frontend: https://auth-project-fullstack.netlify.app/ + +Backend: https://project-auth-api-mnx9.onrender.com/ diff --git a/backend/.gitignore b/backend/.gitignore index 25c8fdbab..8f5e467c8 100644 --- a/backend/.gitignore +++ b/backend/.gitignore @@ -1,2 +1,3 @@ node_modules -package-lock.json \ No newline at end of file +package-lock.json +.env \ No newline at end of file diff --git a/backend/config/db.js b/backend/config/db.js new file mode 100644 index 000000000..a1d061dac --- /dev/null +++ b/backend/config/db.js @@ -0,0 +1,18 @@ + +import mongoose from "mongoose"; +import dotenv from "dotenv"; + +dotenv.config(); + +export const connectDB = async () => { + try { + + const conn = await mongoose.connect(process.env.MONGO_URI); + + console.log(`Mongo DB Connected: ${conn.connection.host}`); + } catch (error) { + console.log(error); + process.exit(1); + } +}; + diff --git a/backend/controllers/userController.js b/backend/controllers/userController.js new file mode 100644 index 000000000..c0db88939 --- /dev/null +++ b/backend/controllers/userController.js @@ -0,0 +1,213 @@ +// import { UserModel } from "../models/userModel"; +// //asyncHandler: We use asyncHandler to simplify error handling in asynchronous code. It helps us avoid writing repetitive try-catch blocks by automatically catching errors and passing them to our error handling middleware. This makes our code cleaner and more readable, reducing the risk of unhandled exceptions that could crash the server. +// import asyncHandler from "express-async-handler"; +// // bcrypt: We use bcrypt to securely hash and store passwords in our database. Storing plain-text passwords is a security risk, as it exposes user credentials in case of a data breach. bcrypt helps us hash passwords in a way that is computationally expensive and time-consuming for potential attackers, making it difficult to crack passwords even if the database is compromised. It enhances the overall security of user authentication in our application. +// import bcrypt from "bcrypt"; +// // jwt (JSON Web Tokens): We use jwt for authentication and authorization. It allows us to create and verify tokens that contain user identity information, such as user IDs or roles. These tokens are often sent with requests to secure routes and verify that a user has the necessary permissions to access certain resources. JWTs are stateless and efficient, making them a popular choice for secure communication between the client and server. +// import jwt from "jsonwebtoken"; + + + +// export const registerUserController = asyncHandler(async (req, res) => { +// // Extract email, username and password from the request body +// const { username, password, email } = req.body; +// // In this try section of the try catch we will first do some conditional logic and then generate the newUser with a crypted password within the DB. +// try { +// // 1st Condition +// // Check wether all fields of registration logic are NOT [!email] inputted from the request.body object +// if (!username || !email || !password) { +// // if so, set http status to a 400code +// res.status(400); +// // and throw new error with some info +// throw new Error("Please add all fields"); +// } +// // 2nd Condition +// // Check if the current user trying to register is using an usernam or email that matches with the same username or email in the database, so they would have to choose something diferent +// const existingUser = await UserModel.findOne({ +// $or: [{ username }, { email }], +// }); +// if (existingUser) { +// res.status(400); +// throw new Error( +// `User with ${existingUser.username === username ? "username" : "email" +// } already exists` +// ); +// } + +// // Generate a salt and hash the user's password +// //In this line below, we're using the bcrypt library to create a random value called "salt." The salt is added to the password before hashing it. It adds an extra layer of security by making it more difficult for attackers to use precomputed tables (rainbow tables) to crack passwords. The 10 in genSaltSync(10) represents the cost factor, which determines how computationally intensive the hashing process will be. +// const salt = bcrypt.genSaltSync(10); + +// const hashedPassword = bcrypt.hashSync(password, salt); +// // In this line below, we're using the generated salt to hash the user's password. Hashing transforms the password into a secure and irreversible string of characters. The bcrypt library handles the entire process for us, ensuring that the password is securely hashed. The resulting hashedPassword is what we store in the database to keep the user's password safe. +// // Create a new user instance with the hashed password +// const newUser = new UserModel({ +// username, +// email, +// password: hashedPassword, +// }); + +// // Mongoose Method: newUser.save() +// // Description: Save the new user instance to the database +// await newUser.save(); + + +// // Respond with a success message, user details, and the JWT token +// res.status(201).json({ +// success: true, +// response: { +// username: newUser.username, +// email: newUser.email, +// id: newUser._id, +// accessToken: newUser.accessToken, +// }, +// }); +// } catch (e) { +// // Handle any errors that occur during the registration process +// res.status(500).json({ success: false, response: e.message }); +// } +// }); + +// // ----------------------- + + +// export const loginUserController = asyncHandler(async (req, res) => { +// // Extract username and password from the request body +// const { username, password } = req.body; + +// try { + +// console.log("Username:", username); +// console.log("Password:", password); +// // Find a user with the provided username in the database +// const user = await UserModel.findOne({ username }); +// if (!user) { +// // If no user is found with the provided username, respond with a 401 Unauthorized and a user not found message +// return res +// .status(401) +// .json({ success: false, response: "User not found" }); +// } + +// // Compare the provided password with the hashed password in the database +// const isMatch = await bcrypt.compare(password, user.password); +// if (!isMatch) { +// // If the provided password doesn't match the stored password, respond with a 401 Unauthorized and an incorrect password message +// return res +// .status(401) +// .json({ success: false, response: "Incorrect password" }); +// } +// // Respond with a success message, user details, and the JWT token +// res.status(200).json({ +// success: true, +// response: { +// username: user.username, +// id: user._id, +// accessToken: user.accessToken, // token for the user using the acessToken generated from the model, // Use the generated token here +// }, +// }); +// } catch (e) { +// // Handle any errors that occur during the login process +// res.status(500).json({ success: false, response: e.message }); +// } +// }); + +import { UserModel } from "../models/userModel"; +import asyncHandler from "express-async-handler"; +import bcrypt from "bcrypt"; +import jwt from "jsonwebtoken"; + +const generateToken = (user) => { + // Function to generate JWT token + return jwt.sign({ id: user._id }, process.env.JWT_SECRET, { + expiresIn: "24h", + }); +}; + +export const registerUserController = asyncHandler(async (req, res) => { + + + const { username, password, email } = req.body; + + try { + + if (!username || !email || !password) { + res.status(400); + throw new Error("Please add all fields"); + } + + const existingUser = await UserModel.findOne({ + $or: [{ username }, { email }], + }); + if (existingUser) { + res.status(400); + throw new Error( + `User with ${existingUser.username === username ? "username" : "email" + } already exists` + ); + } + + + const salt = bcrypt.genSaltSync(10); + const hashedPassword = bcrypt.hashSync(password, salt); + const newUser = new UserModel({ + username, + email, + password: hashedPassword, + }); + + await newUser.save(); + + const token = generateToken(newUser._id); + + res.status(201).json({ + success: true, + response: { + username: newUser.username, + email: newUser.email, + id: newUser._id, + accessToken: token, + }, + }); + } catch (e) { + res.status(500).json({ success: false, response: e.message }); + } +}); + +export const loginUserController = asyncHandler(async (req, res) => { + + const { username, password } = req.body; + + try { + + console.log("Username:", username); + console.log("Password:", password); + + const user = await UserModel.findOne({ username }); + if (!user) { + return res + .status(401) + .json({ success: false, response: "User not found" }); + } + + + const isMatch = await bcrypt.compare(password, user.password); + if (!isMatch) { + + return res + .status(401) + .json({ success: false, response: "Incorrect password" }); + } + const token = generateToken(user._id); + + res.status(200).json({ + success: true, + response: { + username: user.username, + id: user._id, + accessToken: token, + }, + }); + } catch (e) { + res.status(500).json({ success: false, response: e.message }); + } +}); diff --git a/backend/middleware/authenticateUser.js b/backend/middleware/authenticateUser.js new file mode 100644 index 000000000..6a28b312f --- /dev/null +++ b/backend/middleware/authenticateUser.js @@ -0,0 +1,54 @@ +// // Import the UserModel from the User model file +// import { UserModel } from "../models/userModel"; +// // Define a function called authenticateUser that takes a request (req), response (res), and a next function as parameters +// export const authenticateUser = async (req, res, next) => { +// // Retrieve the access token from the request header +// const accessToken = req.header("Authorization"); +// try { +// // Find a user in the database using the retrieved access token +// // Mongoose Method: UserModel.findOne({ accessToken: accessToken }) +// // Description: This line of code serves the purpose of authenticating a user based on the provided access token. It checks if a user with the specified accessToken exists in the database using the UserModel. If a user is found, their user document is stored in the user variable. This allows the middleware to add the user object to the request, making it available for subsequent middleware or routes. If no user is found, it prepares to send a 401 Unauthorized response to indicate that the user needs to log in. This code is an essential part of user authentication in the Node.js application and helps control access to protected routes or endpoints. +// const user = await UserModel.findOne({ accessToken: accessToken }); +// if (user) { +// // If a user is found, add the user object to the request object +// req.user = user; // Add user to the request object +// next(); // Continue to the next middleware or route +// } else { +// // If no user is found, send a 401 Unauthorized response +// res.status(401).json({ success: false, response: "Please log in" }); +// } +// } catch (e) { +// // Handle any errors that occur during the database query or user authentication +// res.status(500).json({ success: false, response: e.message }); +// } +// }; +import { UserModel } from "../models/userModel"; +import jwt from "jsonwebtoken"; + +export const authenticateUser = async (req, res, next) => { + // Retrieve the access token from the request header and remove the "Bearer " prefix if present + const authHeader = req.header("Authorization"); + if (!authHeader || !authHeader.startsWith("Bearer ")) { + return res.status(401).json({ success: false, response: "No token provided, please log in" }); + } + + const token = authHeader.split(' ')[1]; // Get the token from the header + + try { + // Verify token + const decoded = jwt.verify(token, process.env.JWT_SECRET); + + // Fetch the user using the ID decoded from the token + const user = await UserModel.findById(decoded.id).select("-password"); // Exclude password from user data + if (!user) { + return res.status(404).json({ success: false, response: "User not found" }); + } + + // Attach the user to the request object + req.user = user; + next(); // Continue to the next middleware or route handler + } catch (error) { + console.error(`JWT Error: ${error.message}`); + res.status(401).json({ success: false, response: "Invalid token, please log in again" }); + } +}; diff --git a/backend/models/userModel.js b/backend/models/userModel.js new file mode 100644 index 000000000..a388f964c --- /dev/null +++ b/backend/models/userModel.js @@ -0,0 +1,50 @@ +import mongoose from "mongoose"; +import crypto from "crypto"; + +const { Schema } = mongoose; + +const userSchema = new Schema( + { + + username: { + type: String, // Specifies that 'username' should be a string + required: true, // Indicates that 'username' is a required field + unique: true, // Ensures that 'username' values are unique + minlength: 2, // Sets a minimum length of 2 characters for 'username' + }, + // Define the 'password' field with a String data type + password: { + type: String, // Specifies that 'password' should be a string + required: true, // Indicates that 'password' is a required field + minlength: 6, // Sets a minimum length of 6 characters for 'password' + }, + email: { + type: String, + required: true, + unique: true, + }, + //Define the 'accessToken' field with a String data type + accessToken: { + type: String, // Specifies that 'accessToken' should be a string + default: () => crypto.randomBytes(128).toString("hex"), // Sets a default value using a cryptographic random string + }, + }, + { + timestamps: true, + } +); + + +export const UserModel = mongoose.model("User", userSchema); + + + + + + + + + + + + diff --git a/backend/package.json b/backend/package.json index 8de5c4ce0..5266c2f64 100644 --- a/backend/package.json +++ b/backend/package.json @@ -12,9 +12,15 @@ "@babel/core": "^7.17.9", "@babel/node": "^7.16.8", "@babel/preset-env": "^7.16.11", + "bcrypt": "^5.1.1", "cors": "^2.8.5", + "crypto": "^1.0.1", + "dotenv": "^16.3.1", "express": "^4.17.3", - "mongoose": "^8.0.0", + "express-async-handler": "^1.2.0", + "express-list-endpoints": "^6.0.0", + "jsonwebtoken": "^9.0.2", + "mongoose": "^8.0.2", "nodemon": "^3.0.1" } } diff --git a/backend/routes/userRoutes.js b/backend/routes/userRoutes.js new file mode 100644 index 000000000..96c2a5caa --- /dev/null +++ b/backend/routes/userRoutes.js @@ -0,0 +1,20 @@ + +import express from "express"; +import { + registerUserController, + loginUserController, +} from "../controllers/userController"; + + +const router = express.Router(); + + +router.post("/register", registerUserController); + + +router.post("/login", loginUserController); + + +export default router; + + diff --git a/backend/server.js b/backend/server.js index 2d7ae8aa1..ff1c11b0c 100644 --- a/backend/server.js +++ b/backend/server.js @@ -1,27 +1,106 @@ + import express from "express"; import cors from "cors"; -import mongoose from "mongoose"; +import dotenv from "dotenv"; +import expressListEndpoints from "express-list-endpoints"; +import userRoutes from "./routes/userRoutes"; +import { connectDB } from "./config/db"; + +// dotenv.config(); +// const port = process.env.PORT; +// const app = express(); + +// // Add middlewares to enable cors and json body parsing +// app.use(cors()); // Enable CORS (Cross-Origin Resource Sharing) +// app.use(express.json()); // Parse incoming JSON data +// app.use(express.urlencoded({ extended: false })); // Parse URL-encoded data + + + +// app.use(userRoutes); // Use the user-controlled routes for user-related requests + +// //KANSKE SKA ANVÄNDA DOM HÄR +// // app.use(contactRoutes); //for the contactform +// // app.use(mediaRoutes); // for the images/film + + +// // Connection to the database through Mongoose +// connectDB(); +// // Create a dedicated endpoint to view endpoints in the browser +// app.get("/", (req, res) => { +// const endpoints = expressListEndpoints(app); +// res.json(endpoints); +// console.log("List of Endpoints:"); +// console.log(endpoints); +// }); + +// // app.use((err, req, res, next) => { +// // res.status(500).send(err); +// // }); -const mongoUrl = process.env.MONGO_URL || "mongodb://localhost/project-mongo"; -mongoose.connect(mongoUrl, { useNewUrlParser: true, useUnifiedTopology: true }); -mongoose.Promise = Promise; +// app.use((err, req, res, next) => { +// const statusCode = err.statusCode || 500; +// res.status(statusCode).json({ +// message: err.message, +// stack: process.env.NODE_ENV === 'production' ? : err.stack, +// }); +// }); -// 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; +// // Start the server and listen for incoming requests on the specified port +// app.listen(port, () => { +// console.log(`Server running on http://localhost:${port}`); // Display a message when the server is successfully started +// }); + +dotenv.config(); const app = express(); +const port = process.env.PORT; + + +app.use(cors({ + origin: process.env.FRONTEND_URL, + credentials: true, +})); + + +app.use(express.json()); // Parse incoming JSON data +app.use(express.urlencoded({ extended: false })); // Parse URL-encoded data + + + +app.use(userRoutes); + -// Add middlewares to enable cors and json body parsing -app.use(cors()); -app.use(express.json()); +connectDB(); -// Start defining your routes here app.get("/", (req, res) => { - res.send("Hello Technigo!"); + const endpoints = expressListEndpoints(app); + res.json(endpoints); + console.log("List of Endpoints:"); + console.log(endpoints); }); -// Start the server + +app.use((err, req, res, next) => { + console.error(`Error: ${err.message}`); + res.status(500).send('Server Error'); +}); + + app.listen(port, () => { console.log(`Server running on http://localhost:${port}`); }); + + + + + + + + + + + + + + + diff --git a/frontend/package.json b/frontend/package.json index e9c95b79f..e2865e591 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -11,7 +11,11 @@ }, "dependencies": { "react": "^18.2.0", - "react-dom": "^18.2.0" + "react-dom": "^18.2.0", + "react-router-dom": "^6.20.1", + "tailwindcss": "^3.3.6", + "tailwindcss-cli": "^0.1.2", + "zustand": "^4.4.7" }, "devDependencies": { "@types/react": "^18.2.15", @@ -21,6 +25,6 @@ "eslint-plugin-react": "^7.32.2", "eslint-plugin-react-hooks": "^4.6.0", "eslint-plugin-react-refresh": "^0.4.3", - "vite": "^4.4.5" + "vite": "^4.0.3" } -} +} \ No newline at end of file diff --git a/frontend/postcss.config.js b/frontend/postcss.config.js new file mode 100644 index 000000000..2e7af2b7f --- /dev/null +++ b/frontend/postcss.config.js @@ -0,0 +1,6 @@ +export default { + plugins: { + tailwindcss: {}, + autoprefixer: {}, + }, +} diff --git a/frontend/src/App.jsx b/frontend/src/App.jsx index 1091d4310..9968a4201 100644 --- a/frontend/src/App.jsx +++ b/frontend/src/App.jsx @@ -1,3 +1,21 @@ +import { BrowserRouter, Routes, Route } from "react-router-dom"; +import { Login } from "./pages/Login"; // Adjust the import path as needed +import { NotFound } from "./pages/NotFound"; // Adjust the import path as needed +import { Home } from "./pages/Home"; +import "./index.css"; + + export const App = () => { - return
This is a protected area of the app
+ +