diff --git a/README.md b/README.md index dfa05e177..b10ddf7f8 100644 --- a/README.md +++ b/README.md @@ -1,13 +1,15 @@ # Project Auth API -Replace this readme with your own information about your project. +A simple authentication API built with Node.js and Express. It allows users to sign up, log in, and access protected routes with a valid token. The frontend is made with Tailwind CSS. -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? +I didnt understand the first codes that we were provided, I did everything a second time and used videos and lectures that I understood more of. If I had more time I would implement some sort of contact form in the /home directory and work more in the design. + ## 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: https://auth-project-fullstack.netlify.app/ + +Backend: https://project-auth-api-mnx9.onrender.com/ diff --git a/backend/.gitignore b/backend/.gitignore index 25c8fdbab..8f5e467c8 100644 --- a/backend/.gitignore +++ b/backend/.gitignore @@ -1,2 +1,3 @@ node_modules -package-lock.json \ No newline at end of file +package-lock.json +.env \ No newline at end of file diff --git a/backend/config/db.js b/backend/config/db.js new file mode 100644 index 000000000..a1d061dac --- /dev/null +++ b/backend/config/db.js @@ -0,0 +1,18 @@ + +import mongoose from "mongoose"; +import dotenv from "dotenv"; + +dotenv.config(); + +export const connectDB = async () => { + try { + + const conn = await mongoose.connect(process.env.MONGO_URI); + + console.log(`Mongo DB Connected: ${conn.connection.host}`); + } catch (error) { + console.log(error); + process.exit(1); + } +}; + diff --git a/backend/controllers/userController.js b/backend/controllers/userController.js new file mode 100644 index 000000000..c0db88939 --- /dev/null +++ b/backend/controllers/userController.js @@ -0,0 +1,213 @@ +// import { UserModel } from "../models/userModel"; +// //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"; +// // bcrypt: We use bcrypt to securely hash and store passwords in our database. Storing plain-text passwords is a security risk, as it exposes user credentials in case of a data breach. bcrypt helps us hash passwords in a way that is computationally expensive and time-consuming for potential attackers, making it difficult to crack passwords even if the database is compromised. It enhances the overall security of user authentication in our application. +// import bcrypt from "bcrypt"; +// // jwt (JSON Web Tokens): We use jwt for authentication and authorization. It allows us to create and verify tokens that contain user identity information, such as user IDs or roles. These tokens are often sent with requests to secure routes and verify that a user has the necessary permissions to access certain resources. JWTs are stateless and efficient, making them a popular choice for secure communication between the client and server. +// import jwt from "jsonwebtoken"; + + + +// export const registerUserController = asyncHandler(async (req, res) => { +// // Extract email, username and password from the request body +// const { username, password, email } = req.body; +// // In this try section of the try catch we will first do some conditional logic and then generate the newUser with a crypted password within the DB. +// try { +// // 1st Condition +// // Check wether all fields of registration logic are NOT [!email] inputted from the request.body object +// if (!username || !email || !password) { +// // if so, set http status to a 400code +// res.status(400); +// // and throw new error with some info +// throw new Error("Please add all fields"); +// } +// // 2nd Condition +// // Check if the current user trying to register is using an usernam or email that matches with the same username or email in the database, so they would have to choose something diferent +// 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 +// //In this line below, we're using the bcrypt library to create a random value called "salt." The salt is added to the password before hashing it. It adds an extra layer of security by making it more difficult for attackers to use precomputed tables (rainbow tables) to crack passwords. The 10 in genSaltSync(10) represents the cost factor, which determines how computationally intensive the hashing process will be. +// const salt = bcrypt.genSaltSync(10); + +// const hashedPassword = bcrypt.hashSync(password, salt); +// // In this line below, we're using the generated salt to hash the user's password. Hashing transforms the password into a secure and irreversible string of characters. The bcrypt library handles the entire process for us, ensuring that the password is securely hashed. The resulting hashedPassword is what we store in the database to keep the user's password safe. +// // Create a new user instance with the hashed password +// const newUser = new UserModel({ +// username, +// email, +// password: hashedPassword, +// }); + +// // Mongoose Method: newUser.save() +// // Description: Save the new user instance to the database +// await newUser.save(); + + +// // 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 (e) { +// // Handle any errors that occur during the registration process +// res.status(500).json({ success: false, response: e.message }); +// } +// }); + +// // ----------------------- + + +// export const loginUserController = asyncHandler(async (req, res) => { +// // Extract username and password from the request body +// const { username, password } = req.body; + +// try { + +// console.log("Username:", username); +// console.log("Password:", password); +// // 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" }); +// } +// // Respond with a success message, user details, and the JWT token +// res.status(200).json({ +// success: true, +// response: { +// username: user.username, +// id: user._id, +// accessToken: user.accessToken, // 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 }); +// } +// }); + +import { UserModel } from "../models/userModel"; +import asyncHandler from "express-async-handler"; +import bcrypt from "bcrypt"; +import jwt from "jsonwebtoken"; + +const generateToken = (user) => { + // Function to generate JWT token + return jwt.sign({ id: user._id }, process.env.JWT_SECRET, { + expiresIn: "24h", + }); +}; + +export const registerUserController = asyncHandler(async (req, res) => { + + + const { username, password, email } = req.body; + + try { + + if (!username || !email || !password) { + res.status(400); + throw new Error("Please add all fields"); + } + + 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); + const hashedPassword = bcrypt.hashSync(password, salt); + const newUser = new UserModel({ + username, + email, + password: hashedPassword, + }); + + await newUser.save(); + + const token = generateToken(newUser._id); + + res.status(201).json({ + success: true, + response: { + username: newUser.username, + email: newUser.email, + id: newUser._id, + accessToken: token, + }, + }); + } catch (e) { + res.status(500).json({ success: false, response: e.message }); + } +}); + +export const loginUserController = asyncHandler(async (req, res) => { + + const { username, password } = req.body; + + try { + + console.log("Username:", username); + console.log("Password:", password); + + 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" }); + } + const token = generateToken(user._id); + + res.status(200).json({ + success: true, + response: { + username: user.username, + id: user._id, + accessToken: token, + }, + }); + } catch (e) { + res.status(500).json({ success: false, response: e.message }); + } +}); diff --git a/backend/middleware/authenticateUser.js b/backend/middleware/authenticateUser.js new file mode 100644 index 000000000..6a28b312f --- /dev/null +++ b/backend/middleware/authenticateUser.js @@ -0,0 +1,54 @@ +// // Import the UserModel from the User model file +// import { UserModel } from "../models/userModel"; +// // Define a function called authenticateUser that takes a request (req), response (res), and a next function as parameters +// export const authenticateUser = async (req, res, next) => { +// // Retrieve the access token from the request header +// const accessToken = req.header("Authorization"); +// try { +// // Find a user in the database using the retrieved access token +// // Mongoose Method: UserModel.findOne({ accessToken: accessToken }) +// // Description: This line of code serves the purpose of authenticating a user based on the provided access token. It checks if a user with the specified accessToken exists in the database using the UserModel. If a user is found, their user document is stored in the user variable. This allows the middleware to add the user object to the request, making it available for subsequent middleware or routes. If no user is found, it prepares to send a 401 Unauthorized response to indicate that the user needs to log in. This code is an essential part of user authentication in the Node.js application and helps control access to protected routes or endpoints. +// const user = await UserModel.findOne({ accessToken: accessToken }); +// if (user) { +// // If a user is found, add the user object to the request object +// req.user = user; // Add user to the request object +// next(); // Continue to the next middleware or route +// } else { +// // If no user is found, send a 401 Unauthorized response +// res.status(401).json({ success: false, response: "Please log in" }); +// } +// } catch (e) { +// // Handle any errors that occur during the database query or user authentication +// res.status(500).json({ success: false, response: e.message }); +// } +// }; +import { UserModel } from "../models/userModel"; +import jwt from "jsonwebtoken"; + +export const authenticateUser = async (req, res, next) => { + // Retrieve the access token from the request header and remove the "Bearer " prefix if present + const authHeader = req.header("Authorization"); + if (!authHeader || !authHeader.startsWith("Bearer ")) { + return res.status(401).json({ success: false, response: "No token provided, please log in" }); + } + + const token = authHeader.split(' ')[1]; // Get the token from the header + + try { + // Verify token + const decoded = jwt.verify(token, process.env.JWT_SECRET); + + // Fetch the user using the ID decoded from the token + const user = await UserModel.findById(decoded.id).select("-password"); // Exclude password from user data + if (!user) { + return res.status(404).json({ success: false, response: "User not found" }); + } + + // Attach the user to the request object + req.user = user; + next(); // Continue to the next middleware or route handler + } catch (error) { + console.error(`JWT Error: ${error.message}`); + res.status(401).json({ success: false, response: "Invalid token, please log in again" }); + } +}; diff --git a/backend/models/userModel.js b/backend/models/userModel.js new file mode 100644 index 000000000..a388f964c --- /dev/null +++ b/backend/models/userModel.js @@ -0,0 +1,50 @@ +import mongoose from "mongoose"; +import crypto from "crypto"; + +const { Schema } = mongoose; + +const userSchema = new Schema( + { + + username: { + type: String, // Specifies that 'username' should be a string + required: true, // Indicates that 'username' is a required field + unique: true, // Ensures that 'username' values are unique + minlength: 2, // Sets a minimum length of 2 characters for 'username' + }, + // Define the 'password' field with a String data type + password: { + type: String, // Specifies that 'password' should be a string + required: true, // Indicates that 'password' is a required field + minlength: 6, // Sets a minimum length of 6 characters for 'password' + }, + email: { + type: String, + required: true, + unique: true, + }, + //Define the 'accessToken' field with a String data type + accessToken: { + type: String, // Specifies that 'accessToken' should be a string + default: () => crypto.randomBytes(128).toString("hex"), // Sets a default value using a cryptographic random string + }, + }, + { + timestamps: true, + } +); + + +export const UserModel = mongoose.model("User", userSchema); + + + + + + + + + + + + diff --git a/backend/package.json b/backend/package.json index 8de5c4ce0..5266c2f64 100644 --- a/backend/package.json +++ b/backend/package.json @@ -12,9 +12,15 @@ "@babel/core": "^7.17.9", "@babel/node": "^7.16.8", "@babel/preset-env": "^7.16.11", + "bcrypt": "^5.1.1", "cors": "^2.8.5", + "crypto": "^1.0.1", + "dotenv": "^16.3.1", "express": "^4.17.3", - "mongoose": "^8.0.0", + "express-async-handler": "^1.2.0", + "express-list-endpoints": "^6.0.0", + "jsonwebtoken": "^9.0.2", + "mongoose": "^8.0.2", "nodemon": "^3.0.1" } } diff --git a/backend/routes/userRoutes.js b/backend/routes/userRoutes.js new file mode 100644 index 000000000..96c2a5caa --- /dev/null +++ b/backend/routes/userRoutes.js @@ -0,0 +1,20 @@ + +import express from "express"; +import { + registerUserController, + loginUserController, +} from "../controllers/userController"; + + +const router = express.Router(); + + +router.post("/register", registerUserController); + + +router.post("/login", loginUserController); + + +export default router; + + diff --git a/backend/server.js b/backend/server.js index 2d7ae8aa1..ff1c11b0c 100644 --- a/backend/server.js +++ b/backend/server.js @@ -1,27 +1,106 @@ + import express from "express"; import cors from "cors"; -import mongoose from "mongoose"; +import dotenv from "dotenv"; +import expressListEndpoints from "express-list-endpoints"; +import userRoutes from "./routes/userRoutes"; +import { connectDB } from "./config/db"; + +// dotenv.config(); +// const port = process.env.PORT; +// const app = express(); + +// // Add middlewares to enable cors and json body parsing +// app.use(cors()); // Enable CORS (Cross-Origin Resource Sharing) +// app.use(express.json()); // Parse incoming JSON data +// app.use(express.urlencoded({ extended: false })); // Parse URL-encoded data + + + +// app.use(userRoutes); // Use the user-controlled routes for user-related requests + +// //KANSKE SKA ANVÄNDA DOM HÄR +// // app.use(contactRoutes); //for the contactform +// // app.use(mediaRoutes); // for the images/film + + +// // Connection to the database through Mongoose +// connectDB(); +// // Create a dedicated endpoint to view endpoints in the browser +// app.get("/", (req, res) => { +// const endpoints = expressListEndpoints(app); +// res.json(endpoints); +// console.log("List of Endpoints:"); +// console.log(endpoints); +// }); + +// // app.use((err, req, res, next) => { +// // res.status(500).send(err); +// // }); -const mongoUrl = process.env.MONGO_URL || "mongodb://localhost/project-mongo"; -mongoose.connect(mongoUrl, { useNewUrlParser: true, useUnifiedTopology: true }); -mongoose.Promise = Promise; +// app.use((err, req, res, next) => { +// const statusCode = err.statusCode || 500; +// res.status(statusCode).json({ +// message: err.message, +// stack: process.env.NODE_ENV === 'production' ? : err.stack, +// }); +// }); -// 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; +// // Start the server and listen for incoming requests on the specified port +// app.listen(port, () => { +// console.log(`Server running on http://localhost:${port}`); // Display a message when the server is successfully started +// }); + +dotenv.config(); const app = express(); +const port = process.env.PORT; + + +app.use(cors({ + origin: process.env.FRONTEND_URL, + credentials: true, +})); + + +app.use(express.json()); // Parse incoming JSON data +app.use(express.urlencoded({ extended: false })); // Parse URL-encoded data + + + +app.use(userRoutes); + -// Add middlewares to enable cors and json body parsing -app.use(cors()); -app.use(express.json()); +connectDB(); -// Start defining your routes here app.get("/", (req, res) => { - res.send("Hello Technigo!"); + const endpoints = expressListEndpoints(app); + res.json(endpoints); + console.log("List of Endpoints:"); + console.log(endpoints); }); -// Start the server + +app.use((err, req, res, next) => { + console.error(`Error: ${err.message}`); + res.status(500).send('Server Error'); +}); + + app.listen(port, () => { console.log(`Server running on http://localhost:${port}`); }); + + + + + + + + + + + + + + + diff --git a/frontend/package.json b/frontend/package.json index e9c95b79f..e2865e591 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -11,7 +11,11 @@ }, "dependencies": { "react": "^18.2.0", - "react-dom": "^18.2.0" + "react-dom": "^18.2.0", + "react-router-dom": "^6.20.1", + "tailwindcss": "^3.3.6", + "tailwindcss-cli": "^0.1.2", + "zustand": "^4.4.7" }, "devDependencies": { "@types/react": "^18.2.15", @@ -21,6 +25,6 @@ "eslint-plugin-react": "^7.32.2", "eslint-plugin-react-hooks": "^4.6.0", "eslint-plugin-react-refresh": "^0.4.3", - "vite": "^4.4.5" + "vite": "^4.0.3" } -} +} \ No newline at end of file diff --git a/frontend/postcss.config.js b/frontend/postcss.config.js new file mode 100644 index 000000000..2e7af2b7f --- /dev/null +++ b/frontend/postcss.config.js @@ -0,0 +1,6 @@ +export default { + plugins: { + tailwindcss: {}, + autoprefixer: {}, + }, +} diff --git a/frontend/src/App.jsx b/frontend/src/App.jsx index 1091d4310..9968a4201 100644 --- a/frontend/src/App.jsx +++ b/frontend/src/App.jsx @@ -1,3 +1,21 @@ +import { BrowserRouter, Routes, Route } from "react-router-dom"; +import { Login } from "./pages/Login"; // Adjust the import path as needed +import { NotFound } from "./pages/NotFound"; // Adjust the import path as needed +import { Home } from "./pages/Home"; +import "./index.css"; + + export const App = () => { - return
Find me in src/app.jsx!
; + return ( + + + } /> + } /> + } /> + + + ); }; + + + diff --git a/frontend/src/index.css b/frontend/src/index.css index 3e560a674..5962ca5cf 100644 --- a/frontend/src/index.css +++ b/frontend/src/index.css @@ -1,5 +1,13 @@ -:root { +@import './styles/tailwind.css'; + +*, +*::before, +*::after { margin: 0; + padding: 0; +} + +body { font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", "Roboto", "Oxygen", "Ubuntu", "Cantarell", "Fira Sans", "Droid Sans", "Helvetica Neue", sans-serif; diff --git a/frontend/src/main.jsx b/frontend/src/main.jsx index 51294f399..1f685e6b1 100644 --- a/frontend/src/main.jsx +++ b/frontend/src/main.jsx @@ -8,3 +8,6 @@ ReactDOM.createRoot(document.getElementById("root")).render( ); + + + diff --git a/frontend/src/pages/Home.jsx b/frontend/src/pages/Home.jsx new file mode 100644 index 000000000..0423d32be --- /dev/null +++ b/frontend/src/pages/Home.jsx @@ -0,0 +1,50 @@ +import { useNavigate } from "react-router-dom"; +import { userStore } from "../store/userStore"; // Adjust path as needed + +export const Home = () => { + const navigate = useNavigate(); + const logout = userStore(state => state.handleLogout); + + const onSignOut = () => { + logout(); // This should clear the token and update the isLoggedIn state. + navigate('/'); // Redirect the user to the login page. + }; + + return ( +
+

Welcome Home!

+

This is a protected area of the app

+ +
+ ); +}; + + + + + + + + + + + + + +// pages/Home.jsx +// import 'react'; +// import ImageGallery from '../components/ImageGallery'; +// import Header from '../components/Header'; + +// const Home = () => { +// return ( +//
+// ); +// }; + +// export default Home; \ No newline at end of file diff --git a/frontend/src/pages/Login.jsx b/frontend/src/pages/Login.jsx new file mode 100644 index 000000000..bcee470fa --- /dev/null +++ b/frontend/src/pages/Login.jsx @@ -0,0 +1,125 @@ + +import { Link } from "react-router-dom"; +import { userStore } from "../store/userStore"; +import { useState } from "react"; +import { useNavigate } from "react-router-dom"; +import { useEffect } from "react"; + + +export const Login = () => { + + const [loginUsername, setLoginUsername] = useState(""); + const [loginPassword, setLoginPassword] = useState(""); + // State for signup + const [signupUsername, setSignupUsername] = useState(""); + const [signupPassword, setSignupPassword] = useState(""); + const [signupEmail, setSignupEmail] = useState(""); + + const navigate = useNavigate(); + const [message, setMessage] = useState(""); + + + const { handleLogin, isLoggedIn, handleSignup } = userStore((state) => ({ + handleLogin: state.handleLogin, + isLoggedIn: state.isLoggedIn, + handleSignup: state.handleSignup, + })); + + useEffect(() => { + if (isLoggedIn) { + navigate('/home'); + } + }, [isLoggedIn, navigate]); + + const onLoginSubmit = async (e) => { + e.preventDefault(); + setMessage(''); + const result = await handleLogin(loginUsername, loginPassword); + setMessage(result.message); + }; + + + const onSignupSubmit = async (e) => { + e.preventDefault(); + setMessage(''); + const result = await handleSignup(signupUsername, signupPassword, signupEmail); + setMessage(result.message); + setSignupUsername(''); + setSignupPassword(''); + setSignupEmail(''); + }; + + return ( +
+
+

+ Sign Up +

+
+ setSignupUsername(e.target.value)} + /> + setSignupEmail(e.target.value)} + /> + setSignupPassword(e.target.value)} + /> + +
+ +

+ Login +

+
+ setLoginUsername(e.target.value)} + /> + setLoginPassword(e.target.value)} + /> + +
+ {message &&
{message}
} +
+
+ ); +}; + + + + + + + diff --git a/frontend/src/pages/NotFound.jsx b/frontend/src/pages/NotFound.jsx new file mode 100644 index 000000000..df87c634f --- /dev/null +++ b/frontend/src/pages/NotFound.jsx @@ -0,0 +1,18 @@ + +import React from "react"; + +export const NotFound = () => { + return ( +
+

404

+

Page Not Found

+

Sorry, the page you are looking for does not exist.

+ + Go Home + +
+ ); +}; diff --git a/frontend/src/store/userStore.jsx b/frontend/src/store/userStore.jsx new file mode 100644 index 000000000..f5d834606 --- /dev/null +++ b/frontend/src/store/userStore.jsx @@ -0,0 +1,255 @@ +// // Import the 'create' function from the 'zustand' library. +// import { create } from "zustand"; + +// // Get the backend API endpoint from the environment variables. +// const apiEnv = import.meta.env.VITE_API_URL; + +// // Create a Zustand store for user-related state and actions. +// export const userStore = create((set, get) => ({ +// // Initialize username state. +// username: "", +// // Define a function to set the username state. +// setUsername: (username) => set({ username }), + +// // Initialize email state. +// email: "", +// // Define a function to set the email state. +// setEmail: (email) => set({ email }), + +// // Initialize password state. +// password: "", +// // Define a function to set the password state. +// setPassword: (password) => set({ password }), + +// // Initialize accessToken state with null. +// accessToken: null, +// // Define a function to set the accessToken state. +// setAccessToken: (token) => set({ accessToken: token }), + +// // Initialize isLoggedIn state with false. +// isLoggedIn: false, +// // Define a function to set the isLoggedIn state. +// setIsLoggedIn: (isLoggedIn) => set({ isLoggedIn }), + +// // FUNCTION TO REGISTER USERS +// handleSignup: async (username, password, email) => { +// // Check if required fields are provided and display an alert if not. +// if (!username || !password || !email) { +// alert("Please enter username, email, and password"); +// return; +// } + +// try { +// // Send a POST request to the registration endpoint with user data. +// const response = await fetch(`${apiEnv}/register`, { +// method: "POST", +// headers: { +// "Content-Type": "application/json", +// }, +// body: JSON.stringify({ email, username, password }), +// }); + +// // Parse the response data as JSON. +// const data = await response.json(); +// if (data.success) { +// // Update the username state. +// set({ username }); +// // Display a success alert. +// alert("Signup successful!"); +// console.log("Signing up with:", username); +// } else { +// // Display an error message from the server or a generic message. +// alert(data.response || "Signup failed"); +// } +// } catch (error) { +// // Handle and log any signup errors. +// console.error("Signup error:", error); +// alert("An error occurred during signup"); +// } +// }, + +// // LOGIN +// handleLogin: async (username, password) => { +// // Check if both username and password are provided and display an alert if not. +// if (!username || !password) { +// alert("Please enter both username and password"); +// return; +// } + +// try { +// // Send a POST request to the login endpoint with user data. +// const response = await fetch(`${apiEnv}/login`, { +// method: "POST", +// headers: { +// "Content-Type": "application/json", +// }, +// body: JSON.stringify({ username, password }), +// }); + +// // Parse the response data as JSON. +// const data = await response.json(); +// if (data.success) { +// // Update the state with username, accessToken, and set isLoggedIn to true. +// set({ +// username, +// accessToken: data.response.accessToken, +// isLoggedIn: true, +// }); +// // Store the accessToken in the browser's localStorage. +// localStorage.setItem("accessToken", data.response.accessToken); +// // Display a success alert. +// alert("Login successful!"); +// console.log("Logging in with:", username, password); +// return true; // Indicate success +// } else { +// // Display an error message from the server or a generic message. +// alert(data.response || "Login failed"); +// return false; // Indicate failure +// } +// } catch (error) { +// // Handle and log any login errors. +// console.error("Login error:", error); +// alert("An error occurred during login"); +// return false; // Indicate failure +// } +// }, + +// // Function to handle user logout. +// handleLogout: () => { +// // Clear user information and set isLoggedIn to false. +// set({ username: "", accessToken: null, isLoggedIn: false }); +// // Remove the accessToken from localStorage. +// localStorage.removeItem("accessToken"); +// // Additional logout logic can be added here if needed. +// }, +// })); + +// Import the 'create' function from the 'zustand' library. +import { create } from "zustand"; + +// Get the backend API endpoint from the environment variables. +const apiEnv = import.meta.env.VITE_API_URL; +if (!apiEnv) { + console.error('VITE_API_URL environment variable is not set.'); +} + + +// Create a Zustand store for user-related state and actions. +export const userStore = create((set, get) => ({ + // Initialize username state. + username: "", + // Define a function to set the username state. + setUsername: (username) => set({ username }), + + // Initialize email state. + email: "", + // Define a function to set the email state. + setEmail: (email) => set({ email }), + + // Initialize password state. + password: "", + // Define a function to set the password state. + setPassword: (password) => set({ password }), + + // Initialize accessToken state with null. + accessToken: null, + // Define a function to set the accessToken state. + setAccessToken: (token) => set({ accessToken: token }), + + // Initialize isLoggedIn state with false. + isLoggedIn: false, + // Define a function to set the isLoggedIn state. + setIsLoggedIn: (isLoggedIn) => set({ isLoggedIn }), + + // FUNCTION TO REGISTER USERS + handleSignup: async (username, password, email) => { + // Check if required fields are provided and display an alert if not. + if (!username || !password || !email) { + + // alert("Please enter username, email, and password"); + // return; + + return { success: false, message: "Please enter username, email, and password" }; + } + + try { + // Send a POST request to the registration endpoint with user data. + const response = await fetch(`${apiEnv}/register`, { + method: "POST", + headers: { + "Content-Type": "application/json", + }, + body: JSON.stringify({ email, username, password }), + credentials: 'include', + }); + + // Parse the response data as JSON. + const data = await response.json(); + + if (data.success) { + set({ username }); + return { success: true, message: "Signup successful! Please log in." }; + } else { + return { success: false, message: data.response || "Signup failed" }; + } + } catch (error) { + console.error("Signup error:", error); + return { success: false, message: "An error occurred during signup" }; + } + }, + + + + // LOGIN + handleLogin: async (username, password) => { + + if (!username || !password) { + return { success: false, message: "Please enter both username and password" }; + } + + try { + // Send a POST request to the login endpoint with user data. + const response = await fetch(`${apiEnv}/login`, { + method: "POST", + headers: { + "Content-Type": "application/json", + }, + body: JSON.stringify({ username, password }), + credentials: 'include', + }); + + + + if (!response.ok) { + // Handle the non-OK response here + throw new Error(`HTTP error! status: ${response.status}`); + } + + const data = await response.json(); + if (data.success) { + set({ + username, + accessToken: data.response.accessToken, + isLoggedIn: true, + }); + localStorage.setItem("accessToken", data.response.accessToken); + return { success: true, message: "Login successful!" }; + } else { + return { success: false, message: data.response || "Login failed" }; + } + } catch (error) { + console.error("Login error:", error); + return { success: false, message: "Wrong username or password, try again!" }; + } + }, + + // Function to handle user logout. + handleLogout: () => { + // Clear user information and set isLoggedIn to false. + set({ username: "", accessToken: null, isLoggedIn: false }); + // Remove the accessToken from localStorage. + localStorage.removeItem("accessToken"); + // Additional logout logic can be added here if needed. + }, +})); + diff --git a/frontend/src/styles/tailwind.css b/frontend/src/styles/tailwind.css new file mode 100644 index 000000000..7805ee89c --- /dev/null +++ b/frontend/src/styles/tailwind.css @@ -0,0 +1,7 @@ +/* src/styles/tailwind.css */ +@import 'tailwindcss/base'; +@import 'tailwindcss/components'; +@import 'tailwindcss/utilities'; + + +@import url('https://fonts.googleapis.com/css2?family=Archivo+Black&display=swap'); \ No newline at end of file diff --git a/frontend/tailwind.config.js b/frontend/tailwind.config.js new file mode 100644 index 000000000..ab8ad146d --- /dev/null +++ b/frontend/tailwind.config.js @@ -0,0 +1,12 @@ +/** @type {import('tailwindcss').Config} */ +export default { + content: [ + "./src/**/*.{js,jsx,ts,tsx}", + "./index.html", + ], + theme: { + extend: {}, + }, + plugins: [], +} + diff --git a/netlify.toml b/netlify.toml index 95443a1f3..011616c41 100644 --- a/netlify.toml +++ b/netlify.toml @@ -1,6 +1,11 @@ # This file tells netlify where the code for this project is and # how it should build the JavaScript assets to deploy from. [build] - base = "frontend/" - publish = "build/" - command = "npm run build" +base = "frontend" +publish = "dist" +command = "npm run build" + +[[redirects]] +from = "/*" +to = "/index.html" +status = 200 diff --git a/package.json b/package.json index d774b8cc3..791162209 100644 --- a/package.json +++ b/package.json @@ -3,5 +3,12 @@ "version": "1.0.0", "scripts": { "postinstall": "npm install --prefix backend" + }, + "dependencies": { + "bcrypt": "^5.1.1", + "dotenv": "^16.3.1", + "express-list-endpoints": "^6.0.0", + "mongodb": "^6.3.0", + "mongoose": "^8.0.2" } }