Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
60 commits
Select commit Hold shift + click to select a range
d5e8abf
Setting up file structure and importing env and setting up user model
Dec 6, 2023
37ed36f
Setting up the model for the user object
Dec 6, 2023
08fa189
Setting up routes and importing necessary libraries
Dec 6, 2023
8aecba4
Setting up login and register routers
Dec 6, 2023
add6a2d
Add routes to server
Dec 6, 2023
63d25a3
Created middleware check authentication
Dec 8, 2023
bfb164a
Setting up user controllers
Dec 10, 2023
6621747
Refactoring the user routes
Dec 10, 2023
13d9a5b
Debugging typo in controller
Dec 10, 2023
df11ea9
Debugging routes and controllers
Dec 10, 2023
fac02ed
Installing dep. for debugging deployment
Dec 10, 2023
e1242af
Debugging toml to deploy
Dec 10, 2023
b8e4bc9
Creating file structure
Dec 10, 2023
041a2c9
Setting up user store with zustand
Dec 11, 2023
e66087e
Created a navbar
Dec 11, 2023
feb88dc
Added pages and changed naming of routes
Dec 11, 2023
3de51db
Created model for advertisement
Dec 11, 2023
2a82247
Added advert routes
Dec 11, 2023
f69a636
Debugginf import and typo
Dec 11, 2023
bbd2383
Debugging middleware
Dec 11, 2023
34e2db8
Debugging typos to make login and register work
Dec 11, 2023
3ed74fd
Debugging advertModel for deployment
Dec 11, 2023
65796c8
Can't fix cache, keep advertModel
Dec 11, 2023
da60cfb
Created a component that swaps images every 5 seconds
Dec 11, 2023
78dc6bc
Added new keys to the advertmodel
Dec 11, 2023
6c5dd36
Added new keys to the advertmodel
Dec 11, 2023
14a2dc5
Creating a function to display ads
Dec 11, 2023
7d1d31e
Changing controllers to get and add ads
Dec 11, 2023
a90855f
Deleted generateAccessToken
Dec 12, 2023
3e5aad8
General debugging of POST method in BE and adaptations in FE
Dec 14, 2023
23bd14c
Debugging
Dec 14, 2023
2c4547c
Debugging
Dec 14, 2023
b23e941
Took away the ImgUrl from the model
Dec 14, 2023
468e9d5
Final debugging before starting with upload image multer
Dec 14, 2023
ff7ebb7
Adding upload image feature to model
Dec 15, 2023
f573ea9
change key to image and type to buffer
Dec 15, 2023
7446664
Changing to Base64 setup for image storage in MongoDB. Incorporated i…
Dec 15, 2023
d090b69
Created a router to get all ads from all users
Dec 17, 2023
a1a9e97
Add getAllAds endpoint
Dec 18, 2023
7397cae
styling components and added toggle color mode
Dec 18, 2023
4d62401
Last commit before changing image upload setup
Dec 19, 2023
fce639c
Setting up Cloudinary config
Dec 19, 2023
cf8ba9c
Setting up Multer and Cloudinary, controller, route and parser
Dec 19, 2023
a23f6a8
Adding clodinary import in config
Dec 19, 2023
260071c
Adding extra layers of error handling in controller
Dec 19, 2023
05bb0d6
Added ImageID to be able to fetch the specific image ID
Dec 19, 2023
6b28d78
Declare imageId in controller
Dec 19, 2023
eeb95bd
Updating delete and update route handlers to handle Cloudinary data
Dec 19, 2023
22c2118
Added create ad and upload image functionality and fetching cards
Dec 19, 2023
a3c3741
Changes to fetch and populate username
Dec 19, 2023
309ef0d
Added components for error and success messages
Dec 28, 2023
9fec0c2
Added reusable components
Dec 29, 2023
58a49bc
Favicon change and changes to the upload file function
Jan 1, 2024
8f03f45
Added loading message for login and moved the dark mode to home page
Jan 1, 2024
c86be4d
Added burger menu and did necessary adjustment for that
Jan 2, 2024
8fb8700
Styling Changes
Jan 2, 2024
c86d177
Adding readme
Jan 2, 2024
9016a2e
Styling navbar on landing and img
Jan 24, 2024
90d092d
Changing loading message
Jan 24, 2024
4a5b4d7
Changed styling and deleted console logs
Jan 24, 2024
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
65 changes: 61 additions & 4 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,13 +1,70 @@
# Project Auth API

Replace this readme with your own information about your project.
## Description
This project, "Project Authorization Fullstack," is a full-stack application that demonstrates a user authentication system with image upload functionality. The backend is built with Express.js and integrates MongoDB for data persistence, while the frontend is developed using React.

Start by briefly describing the assignment in a sentence or two. Keep it short and to the point.
## Backend - Project Auth Backend

### Features
- User authentication (login, registration, and logout).
- Image upload with Cloudinary integration.
- CRUD operations for ads.
- User and ads management.

### Technologies Used
- Node.js, Express.js
- Mongoose for MongoDB integration.
- bcrypt for password hashing.
- JWT for maintaining user sessions.
- multer and Cloudinary for image uploads.

### Installation
1. Clone the repository.
2. Navigate to the backend directory.
3. Run `npm install` to install dependencies.
4. Create a `.env` file and configure your environment variables (e.g., MongoDB URI, JWT secret, Cloudinary details).

### Usage
- Use `npm start` to run the server.
- Use `npm run dev` for development mode with hot reload.

## Frontend

### Features
- User interface for login, registration, and ad management.
- Responsive design using styled-components.
- State management with Zustand.

### Technologies Used
- React
- React Router for routing.
- Styled-components for styling.
- Zustand for state management.

### Installation
1. Clone the repository.
2. Navigate to the frontend directory.
3. Run `npm install` to install dependencies.

### Usage
- Use `npm run dev` to start the development server.
- Use `npm run build` to create a production build.

## Common

### Installation
- Root directory contains common dependencies and post-install scripts.
- Run `npm install` at the root to set up both frontend and backend.

### Scripts
- `postinstall`: Automatically set up the backend upon installing the root dependencies.

## 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?
If I had more time I would work on the styling and I would create a feature to change/edit a post. Probably I would add the possibility to save a post of someone else and display it under a collection.

## 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.
Backend project deployed at: https://project-authorization-fullstack.onrender.com

Frontend project deployed at: https://fullstack-auth-project.netlify.app/
13 changes: 13 additions & 0 deletions backend/config/cloudinaryConfig.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
import cloudinaryFramework from 'cloudinary';
import dotenv from 'dotenv';

dotenv.config();

// Correct the usage here
cloudinaryFramework.v2.config({
cloud_name: process.env.CLOUDINARY_CLOUD_NAME,
api_key: process.env.CLOUDINARY_API_KEY,
api_secret: process.env.CLOUDINARY_API_SECRET
});

export default cloudinaryFramework.v2;
24 changes: 24 additions & 0 deletions backend/config/db.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
import mongoose from "mongoose";
import dotenv from "dotenv";

// Load environment variables from the .env file
dotenv.config();

// Define an asynchronous function 'connectDB' to connect to the MongoDB database
export const connectDB = async () => {
try {
// Attempt to connect to the MongoDB database using the URL from the environment variables
// Mongoose Method: mongoose.connect()
// Description: This line of code serves the crucial purpose of connecting the Node.js application to the MongoDB database specified by the URL provided in the environment variable MONGO_URL. Once this connection is established, the application can perform various database operations, such as querying and modifying data in the MongoDB database. It's a critical step in setting up the database connection for the application to work with MongoDB.
const conn = await mongoose.connect(process.env.MONGO_URL);

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

// Exit the Node.js process with an exit code of 1 to indicate an error
process.exit(1);
}
};
170 changes: 170 additions & 0 deletions backend/controllers/adController.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,170 @@
import { AdModel } from "../models/AdModel";
//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";
// We need to import the userModel to check for the famous accesstoken
import { UserModel } from "../models/UserModel";
// Import cloudinary configuration
import cloudinary from "../config/cloudinaryConfig";

// desciption: Get Ads
// route: /getAllAds
// access: Private
export const getAllAdsController = asyncHandler(async (req, res) => {
const userStorage = req.user;
const allAds = await AdModel.find().populate("user", "username");
res.status(200).json(allAds);
});


// desciption: Get Ads
// route: /getAds
// access: Private
export const getAdsController = asyncHandler(async (req, res) => {
const userStorage = req.user;
const ads = await AdModel.find({ user: userStorage })
.sort("-createdAt")
.populate("user", "username"); // Populate the user field

res.json(ads);
});

// desciption: POST Ad
// route: /add
// access: Private
export const createAdController = asyncHandler(async (req, res) => {
try {
console.log("Request body:", req.body); // Log the entire request body
console.log("req.file", req.file);
const { brand, model } = req.body;
const accessToken = req.header("Authorization");
const userFromStorage = await UserModel.findOne({ accessToken });

if (!userFromStorage) {
return res.status(401).json({ message: "Unauthorized: User not found." });
}

if (!req.file) {
return res.status(400).json({ message: "No image file provided." });
}

let imageUrl, imageId;
try {
// Upload the image file to Cloudinary
const result = await cloudinary.uploader.upload(req.file.path);
imageUrl = result.url;
imageId = result.public_id; // or use req.file.filename for filename

} catch (uploadError) {
console.error('Cloudinary Upload Error:', uploadError);
return res.status(500).json({ message: "Error uploading image to Cloudinary.", error: uploadError });
}

// Define and save new AD
const newAd = new AdModel({
brand,
model,
image: imageUrl,
imageId: imageId,
user: userFromStorage,
});

const savedAd = await newAd.save();
res.json(savedAd);
} catch (error) {
console.error(error); // Log the detailed error
res.status(500).json({ message: "Internal server error", error });
}
});

// desciption: PUT/PATCH a specific AD
// route: /update/:id
// access: Private
export const updateAdController = asyncHandler(async (req, res) => {
const { id } = req.params;
const updateData = req.body; // This contains the fields to be updated

// Optionally, if you're updating the image, handle the image file upload and get the new image URL and ID
if (req.file) {
try {
// Upload the new image file to Cloudinary
const result = await cloudinary.uploader.upload(req.file.path);
updateData.image = result.url;
updateData.imageId = result.public_id;
} catch (uploadError) {
console.error('Cloudinary Upload Error:', uploadError);
return res.status(500).json({ message: "Error uploading new image to Cloudinary.", error: uploadError });
}
}

// Make sure to check that the user making the update is the owner of the ad
const userFromStorage = await UserModel.findOne({ accessToken: req.header("Authorization") });
if (!userFromStorage) {
return res.status(401).json({ message: "Unauthorized: User not found." });
}

// Update the ad with the new data
AdModel.findByIdAndUpdate(id, updateData, { new: true }) // {new: true} will return the updated document
.then((updatedAd) => {
if (!updatedAd) {
return res.status(404).json({ message: "Ad not found." });
}
res.json(updatedAd);
})
.catch((err) => res.status(500).json({ message: "Error updating ad.", error: err }));
});


// desciption: DELETE all ads
// route: /deleteAll
// access: Private
export const deleteAllAdsController = asyncHandler(async (req, res) => {
const accessToken = req.header("Authorization");

const userFromStorage = await UserModel.findOne({ accessToken });
if (!userFromStorage) {
return res.status(401).json({ message: "Unauthorized: User not found." });
}

// Find all ads for the user
const ads = await AdModel.find({ user: userFromStorage });

// Iterate over all ads and delete associated images from Cloudinary
for (const ad of ads) {
await cloudinary.uploader.destroy(ad.imageId);
}

// After all images are deleted, delete the ads from the database
const result = await AdModel.deleteMany({ user: userFromStorage });
res.json({
message: "All ads and associated images deleted",
deletedCount: result.deletedCount,
});
});

// desciption: DELETE AD by its ID
// route: /delete/:id
// access: Private
export const deleteSpecificAdController = asyncHandler(async (req, res) => {
const { id } = req.params;

const ad = await AdModel.findById(id);
if (!ad) {
return res.status(404).json({ message: "Ad not found" });
}

try {
// Delete the image from Cloudinary using the imageId
await cloudinary.uploader.destroy(ad.imageId);

// Then delete the ad from the database
const result = await AdModel.findByIdAndDelete(id);
res.json({
message: "Ad and associated image deleted successfully",
deletedAd: result,
});
} catch (err) {
console.error('Error during ad deletion:', err);
res.status(500).json({ message: "Failed to delete ad and/or image", error: err });
}
});

92 changes: 92 additions & 0 deletions backend/controllers/userController.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,92 @@
import { UserModel } from "../models/UserModel";
import asyncHandler from "express-async-handler";
import bcrypt from "bcrypt";

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

//Set up a route to handle user registration (Sign-up)
export const registerUserController = asyncHandler(async (req, res) => {

const { username, password, email } = req.body; //defines what to request from the body

try {
if (!username || !email || !password) {
res.status(400);
throw new Error("Please fill in all fields"); //error message shown on the server side
}

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); //Add extra layers of security

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

//create a new user instance with the hashed password
const newUser = new UserModel({
username,
email,
password: hashedPassword, //passes the variable with the encrypted password
});

await newUser.save(); //Mongoose method to save the new user instance to the database

// 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 (err) {
// Handle any errors that occur during the registration process
res.status(500).json({ success: false, response: err.message });
}
});

//Set up a route for logging in
export const loginUserController = asyncHandler(async (req, res) => {
const { username, password } = req.body;

try {
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" });

} res.status(200).json({
success: true,
response: {
username: user.username,
id: user._id,
accessToken: user.accessToken,
}
});
} catch (err) {
// Handle any errors that occur during the login process
res.status(500).json({ success: false, response: err.message });
}
});

Loading