Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Project Auth API-W16 #334

Open
wants to merge 21 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
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
23 changes: 18 additions & 5 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,13 +1,26 @@
# Project Auth API

Replace this readme with your own information about your project.

Start by briefly describing the assignment in a sentence or two. Keep it short and to the point.
Project Auth is a web application designed to provide user authentication functionalities. The application features a frontend built using React.js and a backend developed with Node.js and Express.js. User data is stored securely in a MongoDB database, and password hashing is implemented using bcrypt for enhanced security.

## 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?
Challenges Encountered:

Backend Setup: Setting up the backend with Node.js and Express.js required careful configuration of routes and middleware for authentication.

Frontend Integration: Integrating the frontend with the backend API endpoints and managing authentication state in React was challenging.

User Authentication and Security: Implementing secure authentication and password hashing with bcrypt and JWT authentication required careful implementation.

Cross-Origin Resource Sharing (CORS): Handling CORS issues during development and configuring CORS middleware in the backend were challenges.

Technologies Used:

Frontend: React.js, Axios
Backend: Node.js, Express.js, MongoDB
Authentication: JSON Web Tokens (JWT), bcrypt

## 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.
BE: https://project-auth-2zcr.onrender.com
FE: https://authentication-bmm.netlify.app/
1 change: 1 addition & 0 deletions backend/.gitignore
Original file line number Diff line number Diff line change
@@ -1,2 +1,3 @@
node_modules
.env
package-lock.json
11 changes: 10 additions & 1 deletion backend/package.json
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
{
"type": "module",
"name": "project-auth-backend",
"version": "1.0.0",
"description": "Starter project to get up and running with express quickly",
Expand All @@ -10,11 +11,19 @@
"license": "ISC",
"dependencies": {
"@babel/core": "^7.17.9",
"@babel/node": "^7.16.8",
"@babel/preset-env": "^7.16.11",
"bcrypt": "^5.1.1",
"bcryptjs": "^2.4.3",
Comment on lines +15 to +16
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think one of these is enough?

"cors": "^2.8.5",
"dotenv": "^16.4.5",
"express": "^4.17.3",
"express-async-handler": "^1.2.0",
"express-list-endpoints": "^7.1.0",
"jsonwebtoken": "^9.0.2",
"mongoose": "^8.0.0",
"nodemon": "^3.0.1"
},
"devDependencies": {
"@babel/node": "^7.24.6"
}
}
134 changes: 131 additions & 3 deletions backend/server.js
Original file line number Diff line number Diff line change
@@ -1,15 +1,73 @@
import bcrypt from "bcrypt";
import crypto from "crypto";
import cors from "cors";
import dotenv from "dotenv";
import express from "express";
import jwt from "jsonwebtoken";
import mongoose from "mongoose";
import expressListEndpoints from "express-list-endpoints";
import asyncHandler from "express-async-handler";

const mongoUrl = process.env.MONGO_URL || "mongodb://localhost/project-mongo";
dotenv.config();

const mongoUrl = process.env.MONGO_URL || "mongodb://localhost/project-auth";
mongoose.connect(mongoUrl);
mongoose.Promise = Promise;

const { Schema, model } = mongoose;

const userSchema = new Schema({
username: {
type: String,
unique: true,
required: true,
minlength: 3,
},
email: {
type: String,
unique: true,
required: true,
},
password: {
type: String,
required: true,
required: true,
minlength: 6,
},
accessToken: {
type: String,
default: () => crypto.randomBytes(16).toString("hex"),
},
});

const User = model("User", userSchema);

// Function to generate JWT
const generateAccessToken = (userId) => {
return jwt.sign({ userId }, process.env.ACCESS_TOKEN_SECRET, {
expiresIn: "24h",
});
};
Comment on lines +46 to +50
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fun that you tried out jwt!


// Middleware to athenticate the token
const authenticateToken = async (req, res, next) => {
const authHeader = req.headers["authorization"];
const token = authHeader && authHeader.split(" ")[1];
if (token == null) return res.sendStatus(401);

try {
const decoded = jwt.verify(token, process.env.ACCESS_TOKEN_SECRET);
req.user = await User.findById(decoded.userId);
next();
} catch (err) {
return res.sendStatus(403);
}
};

// 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 || 9000;
const app = express();

// Add middlewares to enable cors and json body parsing
Expand All @@ -18,10 +76,80 @@ app.use(express.json());

// Start defining your routes here
app.get("/", (req, res) => {
res.send("Hello Technigo!");
const endpoints = expressListEndpoints(app);
res.json(endpoints);
});

//Registration Endpoint
//http://localhost:9000/register
app.post("/register", async (req, res) => {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

According to REST naming conventions, endpoints should be named after what they return or in this case create. So this endpoint could be named /users

try {
const { username, email, password } = req.body;
const hashedPassword = bcrypt.hashSync(password, 10);
const user = new User({ username, email, password: hashedPassword });
const savedUser = await user.save();

// Generate access token for the registered user
const accessToken = generateAccessToken(savedUser._id);
res.status(201).json({ id: savedUser._id, accessToken });
} catch (err) {
console.error("Error creating user:", err); // Log the actual error for debugging
let errorMessage = "Could not create user";
if (err.code === 11000) {
// Duplicate key error
if (err.keyPattern && err.keyPattern.username) {
errorMessage = "Username already exists";
} else if (err.keyPattern && err.keyPattern.email) {
errorMessage = "Email already exists";
}
} else if (err.errors) {
// Validation errors
const errorFields = Object.keys(err.errors);
if (errorFields.length > 0) {
errorMessage = err.errors[errorFields[0]].message;
}
}
res.status(400).json({ message: errorMessage, errors: err.errors });
}
});

// Sign-in Endpoint
app.post("/login", async (req, res) => {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

And this could be called /sessions

const { email, password } = req.body;
const user = await User.findOne({ email });

if (!user || !(await bcrypt.compare(password, user.password))) {
return res.status(401).json({ error: "Invalid credentials" });
}

const accessToken = generateAccessToken(user._id);
await User.findByIdAndUpdate(user._id, { accessToken });
res.json({ accessToken, username: user.username });
});

// Authenticated endpoint
app.get("/secrets", authenticateToken, (req, res) => {
res.json({ secret: "This is secret content" });
});

app.get(
"/logged-in",
authenticateToken,
asyncHandler(async (req, res) => {
res.status(200).json({
success: true,
response: {
message: "User is logged in",
},
});
})
);

/* const secret = crypto.randomBytes(64).toString("hex");
console.log(secret); */

// Start the server
app.listen(port, () => {
console.log(`Server running on http://localhost:${port}`);
});
console.log("Contents of process.env:", process.env);
23 changes: 12 additions & 11 deletions frontend/.eslintrc.cjs
Original file line number Diff line number Diff line change
Expand Up @@ -2,19 +2,20 @@ module.exports = {
root: true,
env: { browser: true, es2020: true },
extends: [
'eslint:recommended',
'plugin:react/recommended',
'plugin:react/jsx-runtime',
'plugin:react-hooks/recommended',
"eslint:recommended",
"plugin:react/recommended",
"plugin:react/jsx-runtime",
"plugin:react-hooks/recommended",
],
ignorePatterns: ['dist', '.eslintrc.cjs'],
parserOptions: { ecmaVersion: 'latest', sourceType: 'module' },
settings: { react: { version: '18.2' } },
plugins: ['react-refresh'],
ignorePatterns: ["dist", ".eslintrc.cjs"],
parserOptions: { ecmaVersion: "latest", sourceType: "module" },
settings: { react: { version: "18.2" } },
plugins: ["react-refresh"],
rules: {
'react-refresh/only-export-components': [
'warn',
"react-refresh/only-export-components": [
"warn",
{ allowConstantExport: true },
],
"react/prop-types": "off",
},
}
};
8 changes: 0 additions & 8 deletions frontend/README.md

This file was deleted.

3 changes: 1 addition & 2 deletions frontend/index.html
Original file line number Diff line number Diff line change
@@ -1,8 +1,7 @@
<!doctype html>
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<link rel="icon" type="image/svg+xml" href="/vite.svg" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Vite + React</title>
</head>
Expand Down
9 changes: 9 additions & 0 deletions frontend/netlify.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
[build]
base = "frontend/"
publish = "dist"
command = "npm run build"

[[redirects]]
from = "/*"
to = "/index.html"
status = 200
5 changes: 3 additions & 2 deletions frontend/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,8 @@
"preview": "vite preview"
},
"dependencies": {
"react": "^18.2.0",
"axios": "^1.7.2",
"react": "^18.3.1",
"react-dom": "^18.2.0"
},
"devDependencies": {
Expand All @@ -23,4 +24,4 @@
"eslint-plugin-react-refresh": "^0.4.3",
"vite": "^4.4.5"
}
}
}
1 change: 0 additions & 1 deletion frontend/public/vite.svg

This file was deleted.

35 changes: 34 additions & 1 deletion frontend/src/App.jsx
Original file line number Diff line number Diff line change
@@ -1,3 +1,36 @@
import AuthenticatedContent from "./components/AuthenticatedContent";
import RegistrationForm from "./components/RegistrationForm";
import SignInForm from "./components/SignInForm";
import { useState } from 'react'


export const App = () => {
return <div>Find me in src/app.jsx!</div>;
const [isAuthenticated, setIsAuthenticated] = useState(false)
const [username, setUsername] = useState('')

const handleSigninSuccess = (username) => {
setIsAuthenticated(true)
setUsername(username)
}

const handleSignOut = () => {
setIsAuthenticated(false)
setUsername('')
sessionStorage.removeItem('accessToken')
}

return (
<div className="app-container">
{!isAuthenticated ? (
<div>
<h1>Authentication System</h1>
<RegistrationForm />
<h2>Sign in</h2>
<SignInForm onSignInSuccess={handleSigninSuccess} />
</div>
) : (
<AuthenticatedContent username={username} onSignOut={handleSignOut} />
)}
</div>
);
};
13 changes: 13 additions & 0 deletions frontend/src/components/AuthenticatedContent.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
const AuthenticatedContent = ({ username, onSignOut }) => {
return (
<div className="authenticated-content">
<h2>🎉 Congratulations, {username}! 🎉</h2>
<p>You've successfully signed in!</p>
<p>Your profile is currently under construction, but stay tuned for more exciting features coming your way!</p>
<p>Meanwhile, sit back and relax😉.</p>
<button onClick={onSignOut} className="button">Sign Out</button>
</div>
);
};

export default AuthenticatedContent;
Loading