From 447a6901eb63273a88c6158192058b963de5ea0a Mon Sep 17 00:00:00 2001 From: nishanthkj Date: Tue, 28 Oct 2025 22:38:39 +0530 Subject: [PATCH 1/3] Add JWT-based profile and edit features Implemented JWT authentication in backend, added profile fetch and edit routes, and created corresponding frontend components for protected profile viewing and editing. Updated Navbar and routing to support authentication state and user profile management. --- backend/package.json | 1 + backend/routes/auth.js | 66 +++++++- lib/api.ts | 25 +++ public/profile.svg | 1 + src/Routes/Router.tsx | 35 ++++- src/components/Navbar.tsx | 181 +++++++++++++++++++-- src/components/ProtectedRoute.tsx | 18 +++ src/pages/Editprofile/EditProfile.tsx | 218 ++++++++++++++++++++++++++ src/pages/Login/Login.tsx | 114 +++++++++++--- src/pages/Profile/Profile.tsx | 86 ++++++++++ 10 files changed, 698 insertions(+), 47 deletions(-) create mode 100644 lib/api.ts create mode 100644 public/profile.svg create mode 100644 src/components/ProtectedRoute.tsx create mode 100644 src/pages/Editprofile/EditProfile.tsx create mode 100644 src/pages/Profile/Profile.tsx diff --git a/backend/package.json b/backend/package.json index 9891c08..00102e3 100644 --- a/backend/package.json +++ b/backend/package.json @@ -18,6 +18,7 @@ "dotenv": "^16.4.5", "express": "^4.21.1", "express-session": "^1.18.1", + "jsonwebtoken": "^9.0.2", "mongoose": "^8.8.2", "passport": "^0.7.0", "passport-local": "^1.0.0" diff --git a/backend/routes/auth.js b/backend/routes/auth.js index e26c7a9..3b731fb 100644 --- a/backend/routes/auth.js +++ b/backend/routes/auth.js @@ -2,6 +2,7 @@ const express = require("express"); const passport = require("passport"); const User = require("../models/User"); const router = express.Router(); +const jwt = require("jsonwebtoken"); // Signup route router.post("/signup", async (req, res) => { @@ -23,9 +24,16 @@ router.post("/signup", async (req, res) => { }); // Login route -router.post("/login", passport.authenticate('local'), (req, res) => { - res.status(200).json( { message: 'Login successful', user: req.user } ); -}); +router.post("/login", passport.authenticate("local", { session: false }), (req, res) => { + try { + const user = req.user; + const token = jwt.sign({ id: user.id }, process.env.SESSION_SECRET, { expiresIn: "1d" }); + res.status(200).json({ message: "Login successful", token, user }); + } catch (error) { + res.status(500).json({ message: "Login failed", error: error.message }); + } +} +); // Logout route router.get("/logout", (req, res) => { @@ -39,4 +47,56 @@ router.get("/logout", (req, res) => { }); }); +// ---------------- AUTH MIDDLEWARE ---------------- +function requireAuth(req, res, next) { + const authHeader = req.headers.authorization; + if (!authHeader || !authHeader.startsWith("Bearer ")) { + return res.status(401).json({ message: "Missing or invalid token" }); + } + + const token = authHeader.split(" ")[1]; + try { + const decoded = jwt.verify(token, process.env.SESSION_SECRET); + req.userId = decoded.id; + next(); + } catch (err) { + return res.status(401).json({ message: "Invalid or expired token" }); + } +} + +// ---------------- GET PROFILE ---------------- +router.get("/profile", requireAuth, async (req, res) => { + try { + const user = await User.findById(req.userId).select("-password"); + if (!user) return res.status(404).json({ message: "User not found" }); + res.status(200).json({ user }); + } catch (err) { + res.status(500).json({ message: "Error fetching profile", error: err.message }); + } +}); + +// ---------------- EDIT PROFILE ---------------- +router.put("/profile", requireAuth, async (req, res) => { + try { + const updates = {}; + const { username, email, bio, avatar } = req.body; + + if (username !== undefined) updates.username = username; + if (email !== undefined) updates.email = email; + if (bio !== undefined) updates.bio = bio; + if (avatar !== undefined) updates.avatar = avatar; + + const user = await User.findByIdAndUpdate(req.userId, updates, { + new: true, + runValidators: true, + select: "-password", + }); + + if (!user) return res.status(404).json({ message: "User not found" }); + + res.status(200).json({ message: "Profile updated successfully", user }); + } catch (err) { + res.status(500).json({ message: "Error updating profile", error: err.message }); + } +}); module.exports = router; diff --git a/lib/api.ts b/lib/api.ts new file mode 100644 index 0000000..5b6f7a5 --- /dev/null +++ b/lib/api.ts @@ -0,0 +1,25 @@ +/// +// src/services/api.ts +import axios from "axios"; + +// Backend base URL from .env +const backendUrl = import.meta.env.VITE_BACKEND_URL; + +const api = axios.create({ + baseURL: backendUrl, + headers: { "Content-Type": "application/json" }, +}); + +// Interceptor to attach token from localStorage +api.interceptors.request.use( + (config) => { + const token = localStorage.getItem("token"); + if (token) { + config.headers.Authorization = `Bearer ${token}`; + } + return config; + }, + (error) => Promise.reject(error) +); + +export default api; diff --git a/public/profile.svg b/public/profile.svg new file mode 100644 index 0000000..17efa4c --- /dev/null +++ b/public/profile.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/src/Routes/Router.tsx b/src/Routes/Router.tsx index 40a7861..62e447f 100644 --- a/src/Routes/Router.tsx +++ b/src/Routes/Router.tsx @@ -1,4 +1,5 @@ -import { Routes, Route } from "react-router-dom"; +import { Routes, Route, useNavigate } from "react-router-dom"; +import { useEffect, useState } from "react"; import Tracker from "../pages/Tracker/Tracker.tsx"; import About from "../pages/About/About"; import Contact from "../pages/Contact/Contact"; @@ -7,8 +8,23 @@ import Signup from "../pages/Signup/Signup.tsx"; import Login from "../pages/Login/Login.tsx"; import ContributorProfile from "../pages/ContributorProfile/ContributorProfile.tsx"; import Home from "../pages/Home/Home.tsx"; +import Profile from "../pages/Profile/Profile.tsx"; +import ProtectedRoute from "../components/ProtectedRoute.tsx"; +import EditProfile from "../pages/Editprofile/EditProfile.tsx"; const Router = () => { + const [isAuthenticated, setIsAuthenticated] = useState( + !!localStorage.getItem("token") + ); + useEffect(() => { + const syncAuth = () => setIsAuthenticated(!!localStorage.getItem("token")); + window.addEventListener("authChange", syncAuth); + window.addEventListener("storage", syncAuth); + return () => { + window.removeEventListener("authChange", syncAuth); + window.removeEventListener("storage", syncAuth); + }; + }, []); return ( } /> @@ -19,6 +35,23 @@ const Router = () => { } /> } /> } /> + {/* Protected route */} + + + + } + /> + + + + } + /> ); }; diff --git a/src/components/Navbar.tsx b/src/components/Navbar.tsx index c6cc86d..f8335a1 100644 --- a/src/components/Navbar.tsx +++ b/src/components/Navbar.tsx @@ -1,11 +1,53 @@ -import { Link } from "react-router-dom"; -import { useState, useContext } from "react"; +import { Link, useNavigate, useLocation } from "react-router-dom"; +import { useState, useContext, useEffect } from "react"; import { ThemeContext } from "../context/ThemeContext"; -import { Moon, Sun } from 'lucide-react'; - +import { Moon, Sun } from "lucide-react"; +import { useRef } from "react"; const Navbar: React.FC = () => { + const navigate = useNavigate(); + const location = useLocation(); + + const [menuOpen, setMenuOpen] = useState(false); + const menuRef = useRef(null); + + useEffect(() => { + const onClick = (e: MouseEvent) => { + if (menuRef.current && !menuRef.current.contains(e.target as Node)) + setMenuOpen(false); + }; + + const onStorage = (e: StorageEvent) => { + if (e.key === "token") setIsAuthed(!!e.newValue); + }; + + const onAuthChange = () => { + setIsAuthed(!!localStorage.getItem("token")); + }; + + window.addEventListener("storage", onStorage); + window.addEventListener("authChange", onAuthChange); + window.addEventListener("click", onClick); + return () => { + window.removeEventListener("storage", onStorage); + window.removeEventListener("authChange", onAuthChange); + window.removeEventListener("click", onClick); + }; + }, []); + + useEffect(() => { + setIsOpen(false); // close mobile menu on navigation + }, [location.pathname]); + + const handleLogout = () => { + localStorage.removeItem("token"); + setIsAuthed(false); + navigate("/login"); + }; + const [isAuthed, setIsAuthed] = useState( + !!localStorage.getItem("token") + ); const [isOpen, setIsOpen] = useState(false); const themeContext = useContext(ThemeContext); @@ -46,12 +88,97 @@ const Navbar: React.FC = () => { > Contributors - - Login - + {/* replace your auth block with this */} + {isAuthed ? ( +
+ + + {/* Dropdown Menu */} + {menuOpen && ( +
+ {/* User Info */} +
+

+ {JSON.parse(localStorage.getItem("user") || "{}") + ?.username || "User"} +

+

+ {JSON.parse(localStorage.getItem("user") || "{}") + ?.email || "email@example.com"} +

+
+ + {/* Menu Links */} + setMenuOpen(false)} + > + View Profile + + setMenuOpen(false)} + > + Edit Profile + + +
+ )} +
+ ) : ( + + Login + + )} + + + ) : ( + setIsOpen(false)} + > + Login + + )} + + + ); +} diff --git a/src/pages/Login/Login.tsx b/src/pages/Login/Login.tsx index d6f21a7..259a4b7 100644 --- a/src/pages/Login/Login.tsx +++ b/src/pages/Login/Login.tsx @@ -30,14 +30,30 @@ const Login: React.FC = () => { setIsLoading(true); try { - const response = await axios.post(`${backendUrl}/api/auth/login`, formData); - setMessage(response.data.message); + const response = await axios.post( + `${backendUrl}/api/auth/login`, + formData + ); - if (response.data.message === 'Login successful') { - navigate("/home"); + if (response.status === 200) { + setMessage(response.data.message || "Login successful"); + + localStorage.setItem("token", response.data.token); + localStorage.setItem("user", JSON.stringify(response.data.user)); + + window.dispatchEvent(new Event("authChange")); + + // Redirect after successful login + navigate("/"); + } else { + setMessage(response.data.message || "Login failed"); } } catch (error: any) { - setMessage(error.response?.data?.message || "Something went wrong"); + if (axios.isAxiosError(error) && error.response) { + setMessage(error.response.data?.message || "Invalid email or password"); + } else { + setMessage("Something went wrong. Please try again."); + } } finally { setIsLoading(false); } @@ -53,34 +69,70 @@ const Login: React.FC = () => { > {/* Animated background elements */}
-
-
-
-
+
+
+
+
{/* Branding */}
- Logo + Logo
-

+

GitHubTracker

-

+

Track your GitHub journey

{/* Form Card */} -
-

+
+

Welcome Back

@@ -130,18 +182,24 @@ const Login: React.FC = () => { {/* Message */} {message && ( -
+
{message}
)} {/* Footer Text */}
-

+

Don't have an account? {

-
+
); }; -export default Login; \ No newline at end of file +export default Login; diff --git a/src/pages/Profile/Profile.tsx b/src/pages/Profile/Profile.tsx new file mode 100644 index 0000000..3c07933 --- /dev/null +++ b/src/pages/Profile/Profile.tsx @@ -0,0 +1,86 @@ +import { useEffect, useState } from "react"; +import api from "../../../lib/api"; + +interface User { + username: string; + email: string; + avatarUrl?: string; +} + +const Profile = () => { + const [user, setUser] = useState(null); + const [loading, setLoading] = useState(true); + const [error, setError] = useState(""); + + // ---- Apply theme from localStorage ---- + useEffect(() => { + const savedTheme = localStorage.getItem("theme"); + const root = document.documentElement; + + if (savedTheme === "dark") { + root.classList.add("dark"); + } else { + root.classList.remove("dark"); + } + }, []); + + // ---- Fetch user profile ---- + useEffect(() => { + const fetchProfile = async () => { + try { + const res = await api.get("/api/auth/profile"); + setUser(res.data.user); + } catch (err: any) { + setError(err.response?.data?.message || "Failed to load profile"); + } finally { + setLoading(false); + } + }; + fetchProfile(); + }, []); + + // ---- Loading ---- + if (loading) + return ( +
+

Loading...

+
+ ); + + // ---- Error ---- + if (error) + return ( +
+

{error}

+
+ ); + + // ---- Empty ---- + if (!user) return null; + + // ---- Main UI ---- + return ( +
+
+ avatar +

+ {user.username} +

+

{user.email}

+ + +
+
+ ); +}; + +export default Profile; From 17c5dff61215aca139f08fb3a0021cc14cd154e7 Mon Sep 17 00:00:00 2001 From: Nishanth <138886231+nishanthkj@users.noreply.github.com> Date: Tue, 28 Oct 2025 23:08:19 +0530 Subject: [PATCH 2/3] Update src/components/Navbar.tsx Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com> --- src/components/Navbar.tsx | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/components/Navbar.tsx b/src/components/Navbar.tsx index f8335a1..d16a8f3 100644 --- a/src/components/Navbar.tsx +++ b/src/components/Navbar.tsx @@ -41,7 +41,11 @@ const Navbar: React.FC = () => { const handleLogout = () => { localStorage.removeItem("token"); + localStorage.removeItem("user"); setIsAuthed(false); + setUser(null); + // notify same-tab listeners + window.dispatchEvent(new Event("authChange")); navigate("/login"); }; From ef8ea4ce9ef17af8f30acc2743aab76f65f8aca8 Mon Sep 17 00:00:00 2001 From: Nishanth <138886231+nishanthkj@users.noreply.github.com> Date: Tue, 28 Oct 2025 23:08:47 +0530 Subject: [PATCH 3/3] Update backend/routes/auth.js Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com> --- backend/routes/auth.js | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/backend/routes/auth.js b/backend/routes/auth.js index 3b731fb..bb4246a 100644 --- a/backend/routes/auth.js +++ b/backend/routes/auth.js @@ -79,13 +79,19 @@ router.get("/profile", requireAuth, async (req, res) => { router.put("/profile", requireAuth, async (req, res) => { try { const updates = {}; - const { username, email, bio, avatar } = req.body; + const { username, email, bio, avatar, newPassword } = req.body; if (username !== undefined) updates.username = username; if (email !== undefined) updates.email = email; if (bio !== undefined) updates.bio = bio; if (avatar !== undefined) updates.avatar = avatar; + // Handle password update if newPassword is provided + if (newPassword !== undefined && newPassword.trim().length > 0) { + // Password will be hashed by the User model's pre-save hook + updates.password = newPassword; + } + const user = await User.findByIdAndUpdate(req.userId, updates, { new: true, runValidators: true,