diff --git a/README.md b/README.md index dfa05e177..2bea47405 100644 --- a/README.md +++ b/README.md @@ -1,13 +1,47 @@ # Project Auth API -Replace this readme with your own information about your project. +# Overview -Start by briefly describing the assignment in a sentence or two. Keep it short and to the point. +This project is a full-stack authentication system featuring user registration, login, and access to a protected "secret page" for authenticated users. It integrates both frontend and backend components to deliver a seamless user experience. -## The problem +# Features -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? +- User Registration: Allows new users to create an account. +- User Authentication: Enables users to log in with their credentials. +- Secret Page: Restricted to authenticated users only. + +# Technologies + +# Frontend +- React: A JavaScript library for building user interfaces. (React) +- React Router DOM: For routing and navigation in a React application. (React Router) +- Zustand: A small, fast state management solution for React. (Zustand) +- Vite: A build tool that provides a fast development environment. (Vite) +- ESLint: A tool for identifying and fixing problems in JavaScript code. (ESLint) +- Deployed with Netlify + +# Backend +- Express: A web application framework for Node.js. (Express) +- Node.js: A JavaScript runtime built on Chrome's V8 engine. (Node.js) +- MongoDB Atlas: Cloud database service for MongoDB. (MongoDB Atlas) +- Mongoose: An ODM (Object Data Modeling) library for MongoDB and Node.js. (Mongoose) +- JSON Web Tokens (JWT): For handling authentication and authorization. (JWT) +- Environment Variables: Ensure that environment variables for database connection and other secrets are correctly configured in both the backend and frontend deployment platforms. +- Deployed on Render. + +# Challenges + +- Deployment Issues: Encountered difficulties with deploying both the frontend and backend. Initial challenges included configuring environment variables and ensuring correct integration between frontend and backend. +- Token Management: Faced issues with handling and validating tokens, which required adjustments to ensure secure user authentication. + +# Key Takeaways + +- Integration of Frontend and Backend: Gained experience in connecting a React frontend with an Express backend, including handling authentication and routing. +- Deployment Process: Learned about the deployment process using Netlify and Render, including managing environment variables and troubleshooting deployment issues. +- State Management: Improved understanding of state management with Zustand and how to efficiently manage application state in a React application. +- Database Configuration: Gained practical experience in setting up and configuring MongoDB Atlas with Mongoose for a production-ready database solution. ## 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 on netlify: https://em-authorization.netlify.app/ +Backend on render: https://em-authorization.onrender.com/ diff --git a/backend/.gitignore b/backend/.gitignore index 25c8fdbab..670e318b9 100644 --- a/backend/.gitignore +++ b/backend/.gitignore @@ -1,2 +1,8 @@ node_modules -package-lock.json \ No newline at end of file +package-lock.json +.env +.env +.env.local +.env.development.local +.env.test.local +.env.production.local diff --git a/backend/db.js b/backend/db.js new file mode 100644 index 000000000..fe672d7b6 --- /dev/null +++ b/backend/db.js @@ -0,0 +1,37 @@ +// Configuration file for connecting to MongoDB using Mongoose + +// Importing the Mongoose library for MongoDB interactions +import mongoose from "mongoose"; + +// Importing the dotenv library to manage environment (env) variables +import dotenv from "dotenv"; +// Execute the config function to load variables from .env file into process.env +dotenv.config(); + +/** + * Asynchronous function to connect to MongoDB. + * This function tries to establish a connection with the MongoDB server + * using the connection string provided in the .env file. + */ + +// CONNECT TO LOCAL MONGO DB +export const connectDB = async () => { + // Check if the MONGO_URL environment variable is set + if (!process.env.MONGO_URL) { + console.error("MONGO_URL is not set in .env"); + // Exit the application if MONGO_URL is not set + process.exit(1); + } + + try { + // Attempting to connect to MongoDB using the provided URL and options + const conn = await mongoose.connect(process.env.MONGO_URL); + // Logging a success message with the connected database host + console.log(`MongoDB Atlas Connected: ${conn.connection.host}`); + } catch (error) { + // Logging the error if the connection fails + console.error("MongoDB Atlas connection error:", error); + // Exiting the application in case of connection failure + process.exit(1); + } +}; diff --git a/backend/package.json b/backend/package.json index 8de5c4ce0..5fbdb3e1a 100644 --- a/backend/package.json +++ b/backend/package.json @@ -6,15 +6,27 @@ "start": "babel-node server.js", "dev": "nodemon server.js --exec babel-node" }, + "engines": { + "node": "18.18.0" + }, "author": "", "license": "ISC", "dependencies": { + "@babel/cli": "^7.23.4", "@babel/core": "^7.17.9", - "@babel/node": "^7.16.8", + "@babel/node": "^7.22.19", "@babel/preset-env": "^7.16.11", + "bcrypt": "^5.1.1", + "bcrypt-nodejs": "^0.0.3", "cors": "^2.8.5", + "crypto": "^1.0.1", + "dotenv": "^16.3.1", "express": "^4.17.3", + "express-async-handler": "^1.2.0", + "jsonwebtoken": "^9.0.2", + "mongodb": "^6.3.0", "mongoose": "^8.0.0", - "nodemon": "^3.0.1" + "nodemon": "^3.0.1", + "validator": "^13.11.0" } -} +} \ No newline at end of file diff --git a/backend/server.js b/backend/server.js index 2d7ae8aa1..690fcd9c3 100644 --- a/backend/server.js +++ b/backend/server.js @@ -1,27 +1,40 @@ +// Importing necessary libraries and modules import express from "express"; import cors from "cors"; -import mongoose from "mongoose"; +import dotenv from "dotenv"; +dotenv.config(); // Load and parse environment variables from the .env file +import userRoutes from "./userLogin/routes"; -const mongoUrl = process.env.MONGO_URL || "mongodb://localhost/project-mongo"; -mongoose.connect(mongoUrl, { useNewUrlParser: true, useUnifiedTopology: true }); -mongoose.Promise = Promise; +// Import database connection functions +import { connectDB } from "./db"; -// 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; +// Retrieve the port number from environment variables or set default +const port = process.env.PORT || 3000; + +// Create an Express application instance const app = express(); -// Add middlewares to enable cors and json body parsing +// Middlewares setup app.use(cors()); app.use(express.json()); +app.use(express.urlencoded({ extended: false })); + +// Registering API routes with the Express application +app.use('/', userRoutes); -// Start defining your routes here -app.get("/", (req, res) => { - res.send("Hello Technigo!"); +// Error handling middleware +app.use(function (err, req, res, next) { + console.error(err.stack); + res.status(500).send('Something broke!'); }); -// Start the server +// Connecting to Mongo DB Atlas Instance +connectDB(); // Connects to MongoDB Atlas + +// Start the server and listen for incoming requests app.listen(port, () => { console.log(`Server running on http://localhost:${port}`); }); + + + diff --git a/backend/userLogin/controller.js b/backend/userLogin/controller.js new file mode 100644 index 000000000..c2aab2057 --- /dev/null +++ b/backend/userLogin/controller.js @@ -0,0 +1,116 @@ +import { UserModel } from "./model"; +import asyncHandler from "express-async-handler"; +import bcrypt from "bcrypt"; +import jwt from "jsonwebtoken"; + +const generateToken = (id) => { + return jwt.sign({ id }, process.env.JWT_SECRET, { + expiresIn: '24h' // Make sure token expires in 24 hours + }); +}; + +// Password validation function +const isValidPassword = (password) => { + const hasNumber = /[0-9]/.test(password); + const hasCapitalLetter = /[A-Z]/.test(password); + const hasSpecialSign = /[!@#$%^&*]/.test(password); + return hasNumber && hasCapitalLetter && hasSpecialSign && password.length >= 6; +}; + +// FUNCTION FOR USER REGISTRATION +export const registerUserController = asyncHandler(async (req, res) => { + // Extract email, username & password from the request body + const { username, password, email } = req.body; + + try { + // Check whether all fields of registration are inputted + if (!username || !email || !password) { + res.status(400); + throw new Error("Please add all fields"); + } + + // Check if password meets criteria + if (!isValidPassword(password)) { + return res.status(400).json({ success: false, message: "Password must contain at least one number, one capital letter, and one special character, and be at least 6 characters long." }); + } + + // Check if the username or email already exists in the database + 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 + const salt = bcrypt.genSaltSync(10); + const hashedPassword = bcrypt.hashSync(password, salt); + + // Create a new user instance with the hashed password + const newUser = new UserModel({ username, email, password: hashedPassword }); + await newUser.save(); // Save the new user instance to the database + + // Generate a JWT token for the new user + const token = generateToken(newUser._id); + + // Respond with a success message, user details, and the token + res.status(201).json({ + success: true, + response: { + username: newUser.username, + email: newUser.email, + id: newUser._id, + token + } + }); + } catch (e) { + if (e.name === 'ValidationError') { + // Handle Mongoose validation errors + return res.status(400).json({ success: false, message: e.message }); + } + // Handle other types of errors + res.status(500).json({ success: false, response: e.message }); + } +}); + + +// FUNCTION FOR USER LOGIN +export const loginUserController = asyncHandler(async (req, res) => { + // Extract username and password from the request body + const { username, password } = req.body; + + try { + // 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" }); + } + // Generate a JWT token for the user + const token = generateToken(user._id); + + // Respond with a success message, user details, and the token + res.status(200).json({ + success: true, + response: { + username: user.username, + id: user._id, + token // 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 }); + } +}); + diff --git a/backend/userLogin/middleware.js b/backend/userLogin/middleware.js new file mode 100644 index 000000000..f8a571557 --- /dev/null +++ b/backend/userLogin/middleware.js @@ -0,0 +1,30 @@ + +import jwt from 'jsonwebtoken'; +import { UserModel } from './model'; +import dotenv from 'dotenv'; +dotenv.config(); + +export const authenticateUser = async (req, res, next) => { + const token = req.header('Authorization')?.split(' ')[1]; + if (!token) { + return res.status(401).json({ success: false, message: 'No token, authorization denied' }); + } + + try { + const decoded = jwt.verify(token, process.env.JWT_SECRET); + const user = await UserModel.findById(decoded.id).select('-password'); + + if (!user) { + return res.status(401).json({ success: false, message: 'Token is not valid' }); + } + + if (user.role !== 'user') { + return res.status(403).json({ success: false, message: 'Access denied: requires user role' }); + } + + req.user = user; + next(); + } catch (e) { + res.status(401).json({ success: false, message: 'Token is not valid' }); + } +}; diff --git a/backend/userLogin/model.js b/backend/userLogin/model.js new file mode 100644 index 000000000..9ffbb8442 --- /dev/null +++ b/backend/userLogin/model.js @@ -0,0 +1,46 @@ +import mongoose from "mongoose"; +import validator from 'validator'; + +// Import the Schema class from the Mongoose library +// Destructures the Schema class from the Mongoose library, allowing us to create a schema. +const { Schema } = mongoose; + +// Creates a new Mongoose schema named userSchema that defines the structure of a user document in the MongoDB collection. It includes fields like username, password, and accessToken, specifying their data types, validation rules, and default values. +const userSchema = new Schema( + { + // Define the 'username' field with a String data type + username: { + type: String, // Specifies that 'username' should be a string + required: true, // Indicates that 'username' is a required field + minlength: 5, // Sets a minimum length + unique: true, // Make sure the username is unique in the database + }, + email: { + type: String, + minlength: 4, + required: true, + unique: true, + validate: [validator.isEmail, 'Invalid email address'] + }, + // Define the 'password' field with a String data type + password: { + type: String, + required: true, + minlength: 6 + }, + role: { + type: String, + default: 'user', + }, + }, + { + timestamps: true, + } +); + + +// Create a Mongoose model named 'UserModel' based on the 'userSchema' for the 'users' collection +// This model is used to interact with the "users" collection in the MongoDB database. It allows you to perform CRUD operations on user documents and provides methods for data validation based on the schema. +export const UserModel = mongoose.model("User", userSchema); + + diff --git a/backend/userLogin/routes.js b/backend/userLogin/routes.js new file mode 100644 index 000000000..837972fcd --- /dev/null +++ b/backend/userLogin/routes.js @@ -0,0 +1,30 @@ +// Import the necessary modules and functions +import express from "express"; +import { authenticateUser } from "./middleware"; +import { + registerUserController, + loginUserController, +} from "./controller"; // Import controller functions for user registration and login + +// Create an instance of the Express router +const router = express.Router(); + +// REGISTER ROUTE: Handle user registration +router.post("/register", registerUserController); // When a POST request is made to /register, execute the registerUserController function + +// LOGIN ROUTE: Handle user login +router.post("/login", loginUserController); // When a POST request is made to /login, execute the loginUserController function + +// AUTHENTICATED USER ROUTE: Display a secret message +router.get('/userpage', authenticateUser, (req, res) => { + // Send a secret message to the authenticated user + res.json({ + success: true, + secretMessage: "This is a secret message only for authenticated users!" + }); +}); + +// Export the router for use in the main application +export default router; + +// In summary, this file sets up routes using the Express router for user registration and login operations. It associates each route with the corresponding controller function. These routes define the API endpoints for handling user registration and login within the application. diff --git a/frontend/.gitignore b/frontend/.gitignore index 265f50c92..441ed5559 100644 --- a/frontend/.gitignore +++ b/frontend/.gitignore @@ -23,4 +23,12 @@ dist-ssr *.sln *.sw? -package-lock.json \ No newline at end of file +package-lock.json + +#env +.env +.env +.env.local +.env.development.local +.env.test.local +.env.production.local \ No newline at end of file diff --git a/frontend/index.html b/frontend/index.html index 0c589eccd..f68c2e0f8 100644 --- a/frontend/index.html +++ b/frontend/index.html @@ -1,13 +1,16 @@ -
- - - -