Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
34 commits
Select commit Hold shift + click to select a range
a3af5f1
create model and the first endpoint
Hang-Nguyen-Vu Dec 4, 2023
653760d
add endpoint to find all users
Hang-Nguyen-Vu Dec 5, 2023
ea51a8d
add endpoint for sign in
Hang-Nguyen-Vu Dec 5, 2023
956aba4
create controllers for user
Hang-Nguyen-Vu Dec 6, 2023
296bf31
add user authorization middleware n advert controllers n authorizatio…
Hang-Nguyen-Vu Dec 6, 2023
dd1f89d
add connection to database
Hang-Nguyen-Vu Dec 6, 2023
be27bd3
add package.json at root
Hang-Nguyen-Vu Dec 6, 2023
b8154dd
move endpoint for documentation to server.js
Hang-Nguyen-Vu Dec 6, 2023
110dddc
minor change
Hang-Nguyen-Vu Dec 6, 2023
a83fcc1
set up frontend structure
Hang-Nguyen-Vu Dec 7, 2023
2c75e95
start setting up userStore
Hang-Nguyen-Vu Dec 7, 2023
7235a30
finalize userStore n form n sign out button
Hang-Nguyen-Vu Dec 8, 2023
9d4ebf6
change env variable name
Hang-Nguyen-Vu Dec 8, 2023
a6dadf6
change publish in netlify.toml
Hang-Nguyen-Vu Dec 8, 2023
8359108
move routes into App.jsx to test a new deployment
Hang-Nguyen-Vu Dec 8, 2023
d9aff32
add console log av apiEnv
Hang-Nguyen-Vu Dec 8, 2023
f7a6703
test removing environment variable
Hang-Nguyen-Vu Dec 8, 2023
51b2dbc
debug sign out button to remove accessToken from localStorage
Hang-Nguyen-Vu Dec 8, 2023
99c755e
move useNavigate hook back to form
Hang-Nguyen-Vu Dec 8, 2023
6e87f54
change title in html
Hang-Nguyen-Vu Dec 8, 2023
9d72c9d
debug navigation to authenticated content
Hang-Nguyen-Vu Dec 8, 2023
4f4b671
add comments
Hang-Nguyen-Vu Dec 8, 2023
43afa6f
add advertStore and fetch adverts
Hang-Nguyen-Vu Dec 10, 2023
630d645
remove unnecessary files and add styling
Hang-Nguyen-Vu Dec 10, 2023
df7b962
Update README.md
Hang-Nguyen-Vu Dec 11, 2023
178d860
Update README.md
Hang-Nguyen-Vu Dec 11, 2023
1ba0dfa
add files for adverts
Hang-Nguyen-Vu Dec 11, 2023
27adff0
Merge branch 'master' of https://github.com/Hang-Nguyen-Vu/authentica…
Hang-Nguyen-Vu Dec 11, 2023
ddf582a
replace localhost with backend API
Hang-Nguyen-Vu Dec 11, 2023
90dacbc
debugging middleware
Hang-Nguyen-Vu Dec 11, 2023
a66bcb8
add console log
Hang-Nguyen-Vu Dec 11, 2023
3c4c951
remove Bearer in authorization
Hang-Nguyen-Vu Dec 12, 2023
84a5bf1
change fetchAdverts
Hang-Nguyen-Vu Dec 12, 2023
f635ce0
add funtionality to create advert which hasn't worked yet
Hang-Nguyen-Vu Dec 12, 2023
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
11 changes: 2 additions & 9 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,13 +1,6 @@
# 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.

## 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?

## View it live
Backend: https://hang-authentication-project.onrender.com

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://hang-authentication-project.netlify.app
19 changes: 19 additions & 0 deletions backend/config/db.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
import mongoose from "mongoose";
import dotenv from "dotenv";
dotenv.config();

export const connectDB = async () => {
try {
const mongoUrl = process.env.MONGO_URL || "mongodb://localhost/project-authentication";
const conn = mongoose.connect(mongoUrl);

// If the connection is successful, log a message indicating that the MongoDB is connected
// console.log(`Mongo DB Connected: ${conn.connection.host}`);
} catch (err) {
// If an error occurs during the connection attempt, log the error message
console.log(err);

// Exit the Node.js process with an exit code of 1 to indicate an error
process.exit(1);
};
}
50 changes: 50 additions & 0 deletions backend/controllers/advertControllers.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
import { AdvertModel } from "../models/advert";
import { AdvertiserModel } from "../models/advertiser";
import asyncHandler from "express-async-handler";

export const getOwnAdvertsController = asyncHandler(async (req, res) => {
try {
// Extract accessToken from the request header key "Authorization"
const accessToken = req.header("Authorization");

// Find the user in the database that has the same accessToken
const userFromStorage = await AdvertiserModel.findOne({ accessToken: accessToken});

console.log(userFromStorage);
// Get all the adverts in the database that belong to the user
const userAdverts = await AdvertModel.find({advertiser: userFromStorage}).sort({createdAt: -1});
res.status(200).json(userAdverts);
} catch (err) {
res.status(400).json({ success: false, message: err.message });
}
});

export const addNewAdvertController = asyncHandler(async (req, res) => {
try {
// Extract the advert from the request body
const { product, amount, unit, address, pickupTime } = req.body;

// Extract accessToken from the request header key "Authorization"
const accessToken = req.header("Authorization");

// Find the user in the database that has the same accessToken
const userFromStorage = await AdvertiserModel.findOne({ accessToken: accessToken});

// Add the new advert to the database and attach the user to it
const newAdvert = new AdvertModel({
product: product,
amount: amount,
unit: unit,
address: address,
pickupTime: pickupTime,
advertiser: userFromStorage
});

await newAdvert.save();

// Return the new advert in a response
res.status(201).json(newAdvert);
} catch (err) {
res.status(500).json({ success: false, message: err.message });
}
});
86 changes: 86 additions & 0 deletions backend/controllers/advertiserControllers.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,86 @@
import { AdvertiserModel } from "../models/advertiser";
import asyncHandler from "express-async-handler";
import bcrypt from "bcrypt";
import jwt from "jsonwebtoken";

// Generate a JWT token containing the user's unique ID, with an optional secret key and a 24-hour expiration time
const generateToken = (advertiser) => {
return jwt.sign({ id: advertiser._id}, process.env.JWT_SECRET, {
expiresIn: "1h"
});
};

export const showAllUsersController = asyncHandler(async (req, res) => {
const users = await AdvertiserModel.find();
res.status(200).json(users);
});

export const registerUserController = asyncHandler(async (req, res) => {
const { username, email, password } = req.body;
try {
if (!username || !email || !password) {
res.status(400).json({message: "Please add all fields", error: err.errors});
} else {
const existingUser = await AdvertiserModel.findOne({
$or: [{ username }, { email }]
});

if (existingUser) {
res.status(400).json({message: `User with ${existingUser.username = username ? "username" : "email"} already exists`})
};

const salt = bcrypt.genSaltSync(10);
const hashedPassword = bcrypt.hashSync(password, salt);

const newAdvertiser = new AdvertiserModel({
username,
email,
password: hashedPassword
});

await newAdvertiser.save();

res.status(201).json ({
success: true,
response: {
username: newAdvertiser.username,
email: newAdvertiser.email,
id: newAdvertiser._id,
accessToken: generateToken(newAdvertiser._id)
}
});
};
} catch (err) {
res.status(500).json({ success: false, response: err.message });
}
});

export const signinUserController = asyncHandler(async (req, res) => {
// Retrieve username and password from req.body
const { username, password } = req.body;

// Find a user with the provided username in the database
try {
const user = await AdvertiserModel.findOne({ username });
if (!user) {
// If no user is found with the provided username, return status 401 Unauthorized and message "User not found"
res.status(401).json({ success: false, response: "User not found"});
};

// If a user is found with the provided username, compare the provided password with the hashed password in the database
const isMatch = await bcrypt.compare(password, user.password);
if (!isMatch) {
res.status(401).json({ success: false, response: "Incorrect password"});
};

res.status(200).json({ success: true, response: {
username: user.username,
id: user._id,
// accessToken: generateToken(user._id)
accessToken: user.accessToken
}});

} catch (err) {
res.status(500).json({ success: false, response: err.message});
};
})
26 changes: 26 additions & 0 deletions backend/middlewares/authenticateUser.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
import { AdvertiserModel } from "../models/advertiser";

export const authenticateUser = async (req, res, next) => {
// Extract the accessToken from the headers key "Authorization"
const accessToken = req.header("Authorization");

// Handle missing or invalid tokens
if (!accessToken) {
return res.status(401).json({ success: false, message: "Access token is missing" });
};

// Find the user in the database that has the same accessToken
try {
const user = await AdvertiserModel.findOne({ accessToken: accessToken });

// If that user exists, add the user object to the request object and hand over it to the next middleware or routes. Otherwise, return status 401 Unauthorized and message "Please log in".
if (user) {
req.user = user;
next();
} else {
res.status(401).json({ success: false, message: "Please log in" });
};
} catch (err) {
res.status(500).json({ success: false, message: err.message });
}
};
36 changes: 36 additions & 0 deletions backend/models/advert.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
import mongoose from "mongoose";

const { Schema } = mongoose;

export const advertSchema = new Schema({
product: {
type: String,
required: true
},
amount: {
type: Number,
required: true,
default: 0
},
unit: {
type: String,
required: true
},
address: {
type: String,
required: true
},
pickupTime: {
type: Date,
default: Date.now
},
advertiser: {
type: mongoose.Schema.Types.ObjectId,
ref: "Advertiser"
}
},
{
timestamps: true,
});

export const AdvertModel = mongoose.model("Advert", advertSchema);
34 changes: 34 additions & 0 deletions backend/models/advertiser.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
import mongoose from "mongoose";
import crypto from "crypto";

const { Schema } = mongoose;

export const advertiserSchema = new Schema(
{
username: {
type: String,
unique: true,
required: true,
minlength: 5
},
email: {
type: String,
unique: true,
required: true
},
password: {
type: String,
required: true,
minlength: 6
},
accessToken: {
type: String,
default: () => crypto.randomBytes(128).toString("hex")
}
},
{
timestamps: true // if this is used, mongoose creates both createdAt and updatedAt
}
);

export const AdvertiserModel = mongoose.model("Advertiser", advertiserSchema);
7 changes: 7 additions & 0 deletions backend/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -12,8 +12,15 @@
"@babel/core": "^7.17.9",
"@babel/node": "^7.16.8",
"@babel/preset-env": "^7.16.11",
"bcrypt": "^5.1.1",
"bcrypt-nodejs": "^0.0.3",
"cors": "^2.8.5",
"dotenv": "^16.3.1",
"express": "^4.17.3",
"express-async-handler": "^1.2.0",
"express-list-endpoints": "^6.0.0",
"jsonwebtoken": "^9.0.2",
"mongodb": "^6.3.0",
"mongoose": "^8.0.0",
"nodemon": "^3.0.1"
}
Expand Down
16 changes: 16 additions & 0 deletions backend/routes/advertRoutes.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
import express from "express";
import { authenticateUser } from "../middlewares/authenticateUser";
import {
getOwnAdvertsController,
addNewAdvertController
} from "../controllers/advertControllers";

const router = express.Router();

// An authenticated endpoint which returns only the adverts belonging to the user if the Authorization header with the user's token was correct
router.get("/get", authenticateUser, getOwnAdvertsController);

// An authenticated endpoint for the user to post an advert
router.post("/add", authenticateUser, addNewAdvertController);

export default router;
28 changes: 28 additions & 0 deletions backend/routes/advertiserRoutes.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
import express from "express";
import {
registerUserController,
showAllUsersController,
signinUserController
} from "../controllers/advertiserControllers";

const router = express.Router();

// Endpoint to show all users
router.get(
"/users",
showAllUsersController
);

// Registration endpoint, to create a new user
router.post(
"/register",
registerUserController
);

// Sign-in endpoint, to authenticate a returning user
router.post(
"/signin",
signinUserController
);

export default router;
25 changes: 19 additions & 6 deletions backend/server.js
Original file line number Diff line number Diff line change
@@ -1,9 +1,12 @@
import express from "express";
import cors from "cors";
import mongoose from "mongoose";
import cors from "cors";
import asyncHandler from "express-async-handler";
import listEndpoints from "express-list-endpoints";
import advertiserRoutes from "./routes/advertiserRoutes";
import advertRoutes from "./routes/advertRoutes";
import { connectDB } from "./config/db";

const mongoUrl = process.env.MONGO_URL || "mongodb://localhost/project-mongo";
mongoose.connect(mongoUrl, { useNewUrlParser: true, useUnifiedTopology: true });
mongoose.Promise = Promise;

// Defines the port the app will run on. Defaults to 8080, but can be overridden
Expand All @@ -17,9 +20,19 @@ app.use(cors());
app.use(express.json());

// Start defining your routes here
app.get("/", (req, res) => {
res.send("Hello Technigo!");
});
// Endpoint to show documentation of all endpoints
app.get(
"/",
asyncHandler(async (req, res) => {
const endpoints = listEndpoints(app);
res.json(endpoints);
})
);
app.use(advertiserRoutes);
app.use(advertRoutes);

// Connect to the database
connectDB();

// Start the server
app.listen(port, () => {
Expand Down
1 change: 1 addition & 0 deletions frontend/.eslintrc.cjs
Original file line number Diff line number Diff line change
Expand Up @@ -16,5 +16,6 @@ module.exports = {
'warn',
{ allowConstantExport: true },
],
'react/prop-types': 0
},
}
3 changes: 1 addition & 2 deletions frontend/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,8 @@
<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>
<title>Authentication project - week 16</title>
</head>
<body>
<div id="root"></div>
Expand Down
Loading