Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
32 commits
Select commit Hold shift + click to select a range
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
44 changes: 39 additions & 5 deletions README.md
Original file line number Diff line number Diff line change
@@ -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/
8 changes: 7 additions & 1 deletion backend/.gitignore
Original file line number Diff line number Diff line change
@@ -1,2 +1,8 @@
node_modules
package-lock.json
package-lock.json
.env
.env
.env.local
.env.development.local
.env.test.local
.env.production.local
37 changes: 37 additions & 0 deletions backend/db.js
Original file line number Diff line number Diff line change
@@ -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);
}
};
18 changes: 15 additions & 3 deletions backend/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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"
}
}
}
39 changes: 26 additions & 13 deletions backend/server.js
Original file line number Diff line number Diff line change
@@ -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}`);
});



116 changes: 116 additions & 0 deletions backend/userLogin/controller.js
Original file line number Diff line number Diff line change
@@ -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 });
}
});

30 changes: 30 additions & 0 deletions backend/userLogin/middleware.js
Original file line number Diff line number Diff line change
@@ -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' });
}
};
46 changes: 46 additions & 0 deletions backend/userLogin/model.js
Original file line number Diff line number Diff line change
@@ -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);


Loading