diff --git a/README.md b/README.md index dfa05e177..75ae756a5 100644 --- a/README.md +++ b/README.md @@ -1,13 +1,12 @@ # 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. +Creating my own Authentication API and using it in a simple website. ## 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 used tools like MongoDB Atlas and postman to test my API. I did a small plan for the project using Figma. If I had more time I would work on the strech goals. ## 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 deploy: https://project-auth-backend-kfgp.onrender.com/ +Frontend deploy: https://sweet-lily-05d48d.netlify.app/ 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/models/User.js b/backend/models/User.js new file mode 100644 index 000000000..7292f7712 --- /dev/null +++ b/backend/models/User.js @@ -0,0 +1,11 @@ +import mongoose from "mongoose"; + +const userSchema = new mongoose.Schema({ + username: { type: String, unique: true, required: true }, + password: { type: String, required: true }, + accessToken: { type: String }, +}); + +const User = mongoose.model("User", userSchema); + +export default User; diff --git a/backend/package.json b/backend/package.json index 8de5c4ce0..cbb10f26c 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", + "bcryptjs": "^2.4.3", "cors": "^2.8.5", + "dotenv": "^16.4.5", "express": "^4.17.3", - "mongoose": "^8.0.0", + "jsonwebtoken": "^9.0.2", + "mongoose": "^8.0.0" + }, + "devDependencies": { "nodemon": "^3.0.1" } } diff --git a/backend/server.js b/backend/server.js index dfe86fb8e..405ff3e23 100644 --- a/backend/server.js +++ b/backend/server.js @@ -1,26 +1,110 @@ import cors from "cors"; import express from "express"; import mongoose from "mongoose"; +import bcrypt from "bcrypt"; +import jwt from "jsonwebtoken"; +import dotenv from "dotenv"; +import User from "./models/User"; -const mongoUrl = process.env.MONGO_URL || "mongodb://localhost/project-mongo"; -mongoose.connect(mongoUrl); -mongoose.Promise = Promise; +dotenv.config(); -// 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 app = express(); +const port = process.env.PORT || 8080; -// Add middlewares to enable cors and json body parsing +// Middleware app.use(cors()); app.use(express.json()); -// Start defining your routes here +// Connect to MongoDB +const mongoUrl = process.env.MONGO_URL || "mongodb://localhost/project-auth"; +mongoose.connect(mongoUrl); +mongoose.Promise = Promise; + +// Routes app.get("/", (req, res) => { - res.send("Hello Technigo!"); + res.send("Welcome to your Express API"); }); +app.post("/api/register", async (req, res) => { + const { username, password } = req.body; + + try { + // Check if the username already exists + let user = await User.findOne({ username }); + if (user) { + return res.status(400).json({ message: "Username already taken" }); + } + + // Hash the password + const salt = await bcrypt.genSalt(10); + const hashedPassword = await bcrypt.hash(password, salt); + + // Create a new user + user = new User({ username, password: hashedPassword }); + await user.save(); + + // Generate JWT token + const token = jwt.sign({ id: user._id }, process.env.JWT_SECRET, { + expiresIn: "1h", + }); + + res.status(201).json({ message: "User registered successfully", token }); + } catch (error) { + console.error("Registration error:", error); + res.status(500).json({ message: "Server error" }); + } +}); + +app.post("/api/login", async (req, res) => { + const { username, password } = req.body; + + try { + // Find the user by username + const user = await User.findOne({ username }); + if (!user) { + return res.status(400).json({ message: "Invalid credentials" }); + } + + // Check if the password matches + const isMatch = await bcrypt.compare(password, user.password); + if (!isMatch) { + return res.status(400).json({ message: "Invalid credentials" }); + } + + // Generate JWT token + const token = jwt.sign({ id: user._id }, process.env.JWT_SECRET, { + expiresIn: "1h", + }); + + res.json({ token }); + } catch (error) { + console.error("Login error:", error); + res.status(500).json({ message: "Server error" }); + } +}); + +app.get("/api/protected", authenticateToken, (req, res) => { + res.json({ message: "This is protected content" }); +}); + +function authenticateToken(req, res, next) { + const authHeader = req.headers["authorization"]; + const token = authHeader && authHeader.split(" ")[1]; + + if (!token) { + return res.status(401).json({ message: "No token, authorization denied" }); + } + + jwt.verify(token, process.env.JWT_SECRET, (err, decoded) => { + if (err) { + console.error("Token verification error:", err); + return res.status(403).json({ message: "Token is not valid" }); + } + req.user = decoded; + next(); + }); +} + // Start the server app.listen(port, () => { console.log(`Server running on http://localhost:${port}`); diff --git a/frontend/.gitignore b/frontend/.gitignore index 265f50c92..6254213ab 100644 --- a/frontend/.gitignore +++ b/frontend/.gitignore @@ -23,4 +23,5 @@ dist-ssr *.sln *.sw? -package-lock.json \ No newline at end of file +package-lock.json +.env \ No newline at end of file diff --git a/frontend/index.html b/frontend/index.html index 0c589eccd..04e80fae5 100644 --- a/frontend/index.html +++ b/frontend/index.html @@ -1,10 +1,16 @@ - + - Vite + React + Cute kitties + + +
diff --git a/frontend/package.json b/frontend/package.json index e9c95b79f..cd8de5765 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -10,8 +10,11 @@ "preview": "vite preview" }, "dependencies": { + "axios": "^1.7.2", "react": "^18.2.0", - "react-dom": "^18.2.0" + "react-dom": "^18.2.0", + "react-icons": "^5.2.1", + "react-router-dom": "^6.23.1" }, "devDependencies": { "@types/react": "^18.2.15", diff --git a/frontend/public/Kittens.jpg b/frontend/public/Kittens.jpg new file mode 100644 index 000000000..ced177108 Binary files /dev/null and b/frontend/public/Kittens.jpg differ diff --git a/frontend/public/LogoIcon.png b/frontend/public/LogoIcon.png new file mode 100644 index 000000000..924ecf63c Binary files /dev/null and b/frontend/public/LogoIcon.png differ diff --git a/frontend/public/homeKitten.jpg b/frontend/public/homeKitten.jpg new file mode 100644 index 000000000..5f91cc6e3 Binary files /dev/null and b/frontend/public/homeKitten.jpg differ diff --git a/frontend/src/App.jsx b/frontend/src/App.jsx index 1091d4310..719645227 100644 --- a/frontend/src/App.jsx +++ b/frontend/src/App.jsx @@ -1,3 +1,14 @@ +import { BrowserRouter as Router } from "react-router-dom"; +import AppRoutes from "./routes/AppRoutes"; + export const App = () => { - return
Find me in src/app.jsx!
; + return ( + +
+ +
+
+ ); }; + +export default App; diff --git a/frontend/src/components/Home.jsx b/frontend/src/components/Home.jsx new file mode 100644 index 000000000..07b5e452e --- /dev/null +++ b/frontend/src/components/Home.jsx @@ -0,0 +1,41 @@ +import { Link } from "react-router-dom"; +import { FaLongArrowAltDown } from "react-icons/fa"; +import "../styling/Home.css"; + +const Home = () => { + return ( +
+
+

Ready to see some very cute kitties?

+
+ cute kitten +

+ "A cat will do what it wants, when it wants, and there is not a + thing you can do about it" +

+
+
+ +
+ +
+ +
+

+ To get access to the kittens you need to log in. +

+

Already have an account?

+ + + + +

Need to create an account?

+ + + +
+
+ ); +}; + +export default Home; diff --git a/frontend/src/components/Navbar.jsx b/frontend/src/components/Navbar.jsx new file mode 100644 index 000000000..9f41db25e --- /dev/null +++ b/frontend/src/components/Navbar.jsx @@ -0,0 +1,34 @@ +import { Link } from "react-router-dom"; +import "../styling/Navbar.css"; + +const Navbar = () => { + return ( + + ); +}; + +export default Navbar; diff --git a/frontend/src/components/auth/Login.jsx b/frontend/src/components/auth/Login.jsx new file mode 100644 index 000000000..16f082f20 --- /dev/null +++ b/frontend/src/components/auth/Login.jsx @@ -0,0 +1,62 @@ +import "../../styling/Auth.css"; +import { useState } from "react"; +import axiosInstance from "../axiosInstance"; +import { useNavigate } from "react-router-dom"; + +const Login = () => { + const [username, setUsername] = useState(""); + const [password, setPassword] = useState(""); + const [error, setError] = useState(""); + const navigate = useNavigate(); + + const handleLogin = async (e) => { + e.preventDefault(); + try { + const response = await axiosInstance.post("/api/login", { + username, + password, + }); + const { token } = response.data; + localStorage.setItem("token", token); + console.log("Login successfull"); + navigate("/protected"); + } catch (error) { + setError("Invalid credentials. Please try again"); + console.error("Login error", error); + } + }; + + return ( +
+

Login

+
+ + + +
+ {error &&

{error}

} +
+ ); +}; + +export default Login; diff --git a/frontend/src/components/auth/Register.jsx b/frontend/src/components/auth/Register.jsx new file mode 100644 index 000000000..ae1e7435d --- /dev/null +++ b/frontend/src/components/auth/Register.jsx @@ -0,0 +1,74 @@ +import { useState } from "react"; +import axiosInstance from "../axiosInstance"; +import "../../styling/Auth.css"; +import { useNavigate } from "react-router-dom"; + +const Register = () => { + const [username, setUsername] = useState(""); + const [password, setPassword] = useState(""); + const [error, setError] = useState(""); + const navigate = useNavigate(); + + const handleRegister = async (e) => { + e.preventDefault(); + try { + const response = await axiosInstance.post("api/register", { + username, + password, + }); + + // Extract token from response + const { token } = response.data; + + // Store token in local storage + if (token) { + localStorage.setItem("token", token); + } + + console.log("Registration successfull"); + // Check if token is stored + console.log("Stored token", localStorage.getItem("token")); + + navigate("/protected"); + } catch (error) { + setError("Username already taken or server error. Please try again."); + console.error("Registration error", error); + } + }; + + return ( +
+

Register

+
+ + setUsername(e.target.value)} + /> + + setPassword(e.target.value)} + /> + +
+ {error &&

{error}

} +
+ ); +}; + +export default Register; diff --git a/frontend/src/components/axiosInstance.jsx b/frontend/src/components/axiosInstance.jsx new file mode 100644 index 000000000..8bf4a1a26 --- /dev/null +++ b/frontend/src/components/axiosInstance.jsx @@ -0,0 +1,14 @@ +import axios from "axios"; + +// Create an instance of axios +const axiosInstance = axios.create({ + baseURL: "https://project-auth-backend-kfgp.onrender.com/", + timeout: 10000, + headers: { + "Content-type": "application/json", + }, +}); + +console.log("Axios baseURL:", axiosInstance.defaults.baseURL); + +export default axiosInstance; diff --git a/frontend/src/components/protected/Protected.jsx b/frontend/src/components/protected/Protected.jsx new file mode 100644 index 000000000..a90ccc84f --- /dev/null +++ b/frontend/src/components/protected/Protected.jsx @@ -0,0 +1,59 @@ +import { useEffect, useState } from "react"; +import { useNavigate } from "react-router-dom"; +import axiosInstance from "../axiosInstance"; +import "../../styling/Protected.css"; + +const Protected = () => { + const [protectedData, setProtectedData] = useState(""); + const [error, setError] = useState(""); + const navigate = useNavigate(); + + useEffect(() => { + const fetchProtectedData = async () => { + try { + const token = localStorage.getItem("token"); + if (!token) { + throw new Error("No token found"); + } + + const response = await axiosInstance.get("/api/protected", { + headers: { + Authorization: `Bearer ${token}`, + }, + }); + + setProtectedData(response.data.message); + } catch (error) { + setError("Failed to fetch protected data. Please log in."); + console.error("Protected data error", error); + navigate("/login"); + } + }; + + fetchProtectedData(); + }, [navigate]); + + const handleLogout = () => { + localStorage.removeItem("token"); + navigate("/"); + }; + + return ( +
+
+

Here are the kittens!

+ Cute kittens + {protectedData ? ( +

{protectedData}

+ ) : ( +

{error}

+ )} + +
+
+ ); +}; + +export default Protected; diff --git a/frontend/src/index.css b/frontend/src/index.css index 3e560a674..14be968b7 100644 --- a/frontend/src/index.css +++ b/frontend/src/index.css @@ -1,13 +1,23 @@ :root { margin: 0; - font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", "Roboto", "Oxygen", - "Ubuntu", "Cantarell", "Fira Sans", "Droid Sans", "Helvetica Neue", - sans-serif; + font-family: "Montserrat", "Helvetica Neue", sans-serif; -webkit-font-smoothing: antialiased; -moz-osx-font-smoothing: grayscale; + --dark-color: #ff9f1c; + --light-orange-color: #ffbf69; + --dark-blue-color: #2ec4b6; + --light-blue-color: #cbf3f0; + --pink-color: #ff006e; + + --big-font: "Bebas Neue"; + --medium-font: "Quicksand"; +} + +body { + margin: 0; } code { font-family: source-code-pro, Menlo, Monaco, Consolas, "Courier New", monospace; -} \ No newline at end of file +} diff --git a/frontend/src/routes/AppRoutes.jsx b/frontend/src/routes/AppRoutes.jsx new file mode 100644 index 000000000..dd5c05908 --- /dev/null +++ b/frontend/src/routes/AppRoutes.jsx @@ -0,0 +1,22 @@ +import { Route, Routes } from "react-router-dom"; +import Navbar from "../components/Navbar"; +import Home from "../components/Home"; +import Register from "../components/auth/Register"; +import Login from "../components/auth/Login"; +import Protected from "../components/protected/Protected"; + +const AppRoutes = () => { + return ( + <> + + + } /> + } /> + } /> + } /> + + + ); +}; + +export default AppRoutes; diff --git a/frontend/src/styling/Auth.css b/frontend/src/styling/Auth.css new file mode 100644 index 000000000..aa34790e9 --- /dev/null +++ b/frontend/src/styling/Auth.css @@ -0,0 +1,54 @@ +.authContainer { + padding: 30px; + border: 10px dotted var(--light-orange-color); + width: 350px; + margin: 100px auto; +} + +.authTitle { + font-family: var(--medium-font); + font-size: 28px; +} + +.authForm { + display: flex; + flex-direction: column; +} + +.authLabel { + font-weight: 600; +} + +.textInput { + padding: 5px; + margin-bottom: 10px; + background-color: var(--light-blue-color); + border-radius: 5px; + border: 2px solid var(--dark-blue-color); + display: block; + width: 280px; +} + +.passwordInput { + padding: 5px; + margin-bottom: 20px; + background-color: var(--light-blue-color); + border-radius: 5px; + border: 2px solid var(--dark-blue-color); + display: block; + width: 280px; +} + +.authButton { + padding: 10px; + width: 100px; + border-radius: 10px; + background-color: var(--dark-color); + border: none; + font-size: 20px; + font-family: var(--big-font); +} + +.authButton:hover { + background-color: var(--light-orange-color); +} diff --git a/frontend/src/styling/Home.css b/frontend/src/styling/Home.css new file mode 100644 index 000000000..cb3369af2 --- /dev/null +++ b/frontend/src/styling/Home.css @@ -0,0 +1,93 @@ +.homeContainer { + width: 350px; + margin: 0 auto; +} + +.homeIntro { + border: 10px dotted var(--dark-color); + + margin: 100px 0 20px 0; +} + +.homeTitle { + text-align: center; + margin: 0; + padding-top: 20px; + font-family: var(--medium-font); +} + +.homeSection { + display: flex; + align-items: center; + padding: 20px; +} + +.homeImage { + width: 200px; + height: 200px; + object-fit: cover; + border-radius: 40px; +} + +.homeText { + padding-left: 10px; + font-size: 20px; + font-family: var(--big-font); + color: var(--blue-dark-color); +} + +.arrowIcon { + font-size: 50px; + display: flex; + justify-content: center; + margin-bottom: 20px; + color: var(--pink-color); +} + +.homeAccount { + display: flex; + flex-direction: column; + padding: 30px 20px; + margin: 50px 0 100px 0; + background-color: var(--light-blue-color); + border-radius: 20px; +} + +.accountIntro { + font-size: 20px; + font-weight: 600; +} + +.accountButton { + padding: 10px 20px; + border-radius: 10px; + border: none; + background-color: var(--dark-color); + font-family: Montserrat; + font-weight: 600; + color: white; +} + +.accountButton:hover { + background-color: var(--light-orange-color); +} + +@media (min-width: 1024px) { + .homeContainer { + width: 550px; + } +} + +@media (min-width: 1600px) { + .homeContainer { + width: 750px; + } + + .accountText { + font-size: 20px; + } + + .accountButton { + font-size: 20px; + } +} diff --git a/frontend/src/styling/Navbar.css b/frontend/src/styling/Navbar.css new file mode 100644 index 000000000..3bc99accd --- /dev/null +++ b/frontend/src/styling/Navbar.css @@ -0,0 +1,61 @@ +.navbar { + background-color: var(--dark-color); + padding: 5px 30px; + width: 100%; +} + +.navTitleSection { + display: flex; + align-items: center; +} + +.navTitle { + color: white; + font-size: 30px; +} + +.logoIcon { + height: 50px; + width: 50px; +} + +.navList { + align-items: center; + padding: 0; + display: flex; +} + +.navItem { + margin-right: 20px; + list-style-type: none; +} + +.navLink { + color: white; + text-decoration: none; + font-weight: 600; +} + +.navLink:hover { + color: #cbf3f0; +} + +@media (min-width: 745px) { + .navTitle { + font-size: 40px; + } + + .navList { + font-size: 20px; + } +} + +@media (min-width: 1024px) { + .navTitle { + font-size: 50px; + } + + .navList { + font-size: 26px; + } +} diff --git a/frontend/src/styling/Protected.css b/frontend/src/styling/Protected.css new file mode 100644 index 000000000..2e71e3aa7 --- /dev/null +++ b/frontend/src/styling/Protected.css @@ -0,0 +1,28 @@ +.protectedWrapper { + width: 100%; + margin: 100px 0; +} + +.protectedContainer { + border: 20px dotted var(--dark-blue-color); + display: flex; + flex-direction: column; + align-items: center; + width: 350px; + margin: 0 auto; + padding: 20px; +} + +.protectedTitle { + font-family: var(--big-font); +} + +.protectedImage { + height: 300px; + width: 350px; + object-fit: cover; +} + +.protectedMessage { + margin: 30px; +} diff --git a/netlify.toml b/netlify.toml index ed9e83391..0c8656403 100644 --- a/netlify.toml +++ b/netlify.toml @@ -1,6 +1,10 @@ -# 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/" + base = "frontend" publish = "dist" command = "npm run build" + +[[redirects]] + from = "/*" + to = "/index.html" + status = 200 + \ No newline at end of file diff --git a/package.json b/package.json index d774b8cc3..10b14c9d8 100644 --- a/package.json +++ b/package.json @@ -1,5 +1,5 @@ { - "name": "project-auth-parent", + "name": "project-auth", "version": "1.0.0", "scripts": { "postinstall": "npm install --prefix backend"