From 81db477703980efd5124856c00755632da7b7bfc Mon Sep 17 00:00:00 2001 From: anshggss Date: Sun, 24 May 2026 19:36:51 +0530 Subject: [PATCH 1/7] Updated mongoose user model --- backend/models/User.js | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/backend/models/User.js b/backend/models/User.js index eb506ed5..6be64391 100644 --- a/backend/models/User.js +++ b/backend/models/User.js @@ -16,11 +16,15 @@ const UserSchema = new mongoose.Schema({ type: String, required: true, }, + token: { + type: String, + required: true, + }, }); // ✅ FIXED: no next() -UserSchema.pre('save', async function () { - if (!this.isModified('password')) return; +UserSchema.pre("save", async function () { + if (!this.isModified("password")) return; const salt = await bcrypt.genSalt(10); this.password = await bcrypt.hash(this.password, salt); @@ -31,4 +35,5 @@ UserSchema.methods.comparePassword = async function (enteredPassword) { return bcrypt.compare(enteredPassword, this.password); }; -module.exports = mongoose.model("User", UserSchema); \ No newline at end of file +module.exports = mongoose.model("User", UserSchema); + From f0620bdbd4f1e707b476fa79ff28d0d713abf712 Mon Sep 17 00:00:00 2001 From: anshggss Date: Sun, 24 May 2026 20:52:21 +0530 Subject: [PATCH 2/7] Fix: changed token type in user schema --- backend/models/User.js | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/backend/models/User.js b/backend/models/User.js index 6be64391..cd85ea6d 100644 --- a/backend/models/User.js +++ b/backend/models/User.js @@ -18,7 +18,8 @@ const UserSchema = new mongoose.Schema({ }, token: { type: String, - required: true, + unique: true, + sparse: true, }, }); From 4dd08d04d6f8d073bced67f4acf7c484e137a4c3 Mon Sep 17 00:00:00 2001 From: anshggss Date: Sun, 24 May 2026 20:55:30 +0530 Subject: [PATCH 3/7] feat: added a post route for saving token in the mongo --- backend/routes/auth.js | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) diff --git a/backend/routes/auth.js b/backend/routes/auth.js index 7c2cda78..6dfe7559 100644 --- a/backend/routes/auth.js +++ b/backend/routes/auth.js @@ -35,6 +35,24 @@ router.post("/login", validateRequest(loginSchema), passport.authenticate('local res.status(200).json( { message: 'Login successful', user: req.user } ); }); +// Save GitHub token route +router.post("/token", async (req, res) => { + if (!req.isAuthenticated()) { + return res.status(401).json({ message: 'Not authenticated' }); + } + const { token } = req.body; + if (!token) { + return res.status(400).json({ message: 'Token is required' }); + } + try { + await User.findByIdAndUpdate(req.user._id, { token }); + req.user.token = token; + res.status(200).json({ success: true, message: 'Token saved successfully' }); + } catch (err) { + res.status(500).json({ message: 'Error saving token', error: err.message }); + } +}); + // Logout route router.get("/logout", (req, res) => { From 4814dd4dad0aa4390474181967af4034a263b889 Mon Sep 17 00:00:00 2001 From: anshggss Date: Sun, 24 May 2026 20:58:15 +0530 Subject: [PATCH 4/7] feat: modified passport field to add auth token --- backend/config/passportConfig.js | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/backend/config/passportConfig.js b/backend/config/passportConfig.js index 842f50ca..d1c88b04 100644 --- a/backend/config/passportConfig.js +++ b/backend/config/passportConfig.js @@ -20,7 +20,8 @@ passport.use( return done(null, { id : user._id.toString(), username: user.username, - email: user.email + email: user.email, + token: user.token }); } catch (err) { return done(err); From 210bf0ad87571a0ef7820d0d2e556be7984fd032 Mon Sep 17 00:00:00 2001 From: anshggss Date: Sun, 24 May 2026 21:06:30 +0530 Subject: [PATCH 5/7] feat: added setToken route to enter token on logging in and wrapped application in UserProvider to provide user context --- src/App.tsx | 2 +- src/Routes/Router.tsx | 10 ++++++---- src/context/UserContext.tsx | 40 +++++++++++++++++++++++++++++++++++++ src/main.tsx | 13 +++++++----- 4 files changed, 55 insertions(+), 10 deletions(-) create mode 100644 src/context/UserContext.tsx diff --git a/src/App.tsx b/src/App.tsx index 8eafb448..c8b453ba 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -5,7 +5,7 @@ import ScrollProgressBar from "./components/ScrollProgressBar"; import { Toaster } from "react-hot-toast"; import Router from "./Routes/Router"; -const FULLSCREEN_ROUTES = ["/signup", "/login"]; +const FULLSCREEN_ROUTES = ["/signup", "/login", "/enterToken"]; function App() { const location = useLocation(); diff --git a/src/Routes/Router.tsx b/src/Routes/Router.tsx index 874ef7e7..1fcb2139 100644 --- a/src/Routes/Router.tsx +++ b/src/Routes/Router.tsx @@ -7,8 +7,10 @@ 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 Activity from "../pages/Activity.tsx"; -import PrivacyPolicy from "../pages/Privacy/PrivacyPolicy.tsx"; // ✅ Updated import path to match your new folder structure +import Activity from "../pages/Activity.tsx"; +import PrivacyPolicy from "../pages/Privacy/PrivacyPolicy.tsx"; +import SetToken from "../components/SetToken.tsx"; +import Profile from "../pages/Profile/Profile.tsx"; const Router = () => { return ( @@ -22,9 +24,9 @@ const Router = () => { } /> } /> } /> - - {/* Privacy Policy page route */} } /> + } /> + } /> ); }; diff --git a/src/context/UserContext.tsx b/src/context/UserContext.tsx new file mode 100644 index 00000000..3e099da9 --- /dev/null +++ b/src/context/UserContext.tsx @@ -0,0 +1,40 @@ +import React, { createContext, useContext, useState } from "react"; + +export interface UserData { + _id: string; + username: string; + email: string; + token?: string; +} + +interface UserContextType { + user: UserData | null; + setUser: (user: UserData | null) => void; + updateToken: (token: string) => void; +} + +export const UserContext = createContext(null); + +export const useUser = () => { + const ctx = useContext(UserContext); + if (!ctx) throw new Error("useUser must be used within UserProvider"); + return ctx; +}; + +const UserProvider: React.FC<{ children: React.ReactNode }> = ({ children }) => { + const [user, setUserState] = useState(null); + + const setUser = (u: UserData | null) => setUserState(u); + + const updateToken = (token: string) => { + setUserState((prev) => (prev ? { ...prev, token } : prev)); + }; + + return ( + + {children} + + ); +}; + +export default UserProvider; diff --git a/src/main.tsx b/src/main.tsx index 4c5b79dd..e3e20374 100644 --- a/src/main.tsx +++ b/src/main.tsx @@ -4,13 +4,16 @@ import App from "./App.tsx"; import "./index.css"; import { BrowserRouter } from "react-router-dom"; import ThemeWrapper from "./context/ThemeContext.tsx"; +import UserProvider from "./context/UserContext.tsx"; createRoot(document.getElementById("root")!).render( - - - - - + + + + + + + ); \ No newline at end of file From cd0e174257d68c84a5d053d85af08095f749a6a6 Mon Sep 17 00:00:00 2001 From: anshggss Date: Sun, 24 May 2026 21:11:59 +0530 Subject: [PATCH 6/7] feat: redirect to SetToken page if user has no token saved --- src/components/SetToken.tsx | 135 ++++++++++++++++++++++++++++++++++++ src/pages/Login/Login.tsx | 93 ++++++++++++++++++------- 2 files changed, 202 insertions(+), 26 deletions(-) create mode 100644 src/components/SetToken.tsx diff --git a/src/components/SetToken.tsx b/src/components/SetToken.tsx new file mode 100644 index 00000000..ddfb19db --- /dev/null +++ b/src/components/SetToken.tsx @@ -0,0 +1,135 @@ +import React, { useState, useContext } from "react"; +import axios from "axios"; +import { useNavigate } from "react-router-dom"; +import { ThemeContext } from "../context/ThemeContext"; +import type { ThemeContextType } from "../context/ThemeContext"; +import { useUser } from "../context/UserContext"; +import { KeyIcon, Eye, EyeOff } from "lucide-react"; + +const backendUrl = import.meta.env.VITE_BACKEND_URL; + +const SetToken: React.FC = () => { + const [token, setToken] = useState(""); + const [message, setMessage] = useState(""); + const [isLoading, setIsLoading] = useState(false); + const [showToken, setShowToken] = useState(false); + const navigate = useNavigate(); + const themeContext = useContext(ThemeContext) as ThemeContextType; + const { mode } = themeContext; + const { updateToken } = useUser(); + + const handleSubmit = async (e: React.FormEvent) => { + e.preventDefault(); + setIsLoading(true); + try { + const response = await axios.post( + `${backendUrl}/api/auth/token`, + { token }, + { withCredentials: true } + ); + if (response.data.success) { + updateToken(token); + navigate("/"); + } + } catch { + setMessage("Failed to save token. Make sure you are logged in."); + } finally { + setIsLoading(false); + } + }; + + return ( +
+
+
+
+
+ +
+
+
+ +
+

+ GitHub Token +

+

+ Enter your Personal Access Token to get started +

+
+ +
+
+
+
+ setToken(e.target.value)} + required + className={`w-full pl-4 pr-12 py-4 rounded-2xl focus:outline-none transition-all ${ + mode === "dark" + ? "bg-white/5 border border-white/10 text-white placeholder-slate-400 focus:ring-2 focus:ring-purple-500" + : "bg-gray-100 border border-gray-300 text-gray-900 placeholder-gray-500 focus:ring-2 focus:ring-purple-400" + }`} + /> + +
+

+ + Generate a new token + + {" "}with repo and read:user scopes. +

+
+ + + + +
+ + {message && ( +
+ {message} +
+ )} +
+
+
+ ); +}; + +export default SetToken; diff --git a/src/pages/Login/Login.tsx b/src/pages/Login/Login.tsx index 92b7073e..ff9812aa 100644 --- a/src/pages/Login/Login.tsx +++ b/src/pages/Login/Login.tsx @@ -3,6 +3,7 @@ import axios from "axios"; import { useNavigate, Link } from "react-router-dom"; import { ThemeContext } from "../../context/ThemeContext"; import type { ThemeContextType } from "../../context/ThemeContext"; +import { useUser } from "../../context/UserContext"; const backendUrl = import.meta.env.VITE_BACKEND_URL; @@ -12,13 +13,16 @@ interface LoginFormData { } const Login: React.FC = () => { - const [formData, setFormData] = useState({ email: "", password: "" }); + const [formData, setFormData] = useState({ + email: "", + password: "", + }); const [message, setMessage] = useState(""); const [isLoading, setIsLoading] = useState(false); - const navigate = useNavigate(); const themeContext = useContext(ThemeContext) as ThemeContextType; const { mode } = themeContext; + const { setUser } = useUser(); const handleChange = (e: ChangeEvent) => { const { name, value } = e.target; @@ -30,11 +34,21 @@ const Login: React.FC = () => { setIsLoading(true); try { - const response = await axios.post(`${backendUrl}/api/auth/login`, formData); + const response = await axios.post( + `${backendUrl}/api/auth/login`, + formData, + { withCredentials: true }, + ); setMessage(response.data.message); - if (response.data.message === 'Login successful') { - navigate("/"); + if (response.data.message === "Login successful") { + const userData = response.data.user; + setUser(userData); + if (!userData.token) { + navigate("/enterToken"); + } else { + navigate("/"); + } } } catch (error: unknown) { if (axios.isAxiosError(error)) { @@ -57,34 +71,54 @@ const Login: React.FC = () => { > {/* Animated background elements */}
-
-
-
-
+
+
+
+
{/* Branding */}
- Logo + Logo
-

+

GitHubTracker

-

+

Track your GitHub journey

{/* Form Card */} -
-

+
+

Welcome Back

@@ -134,18 +168,22 @@ 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; + From b4c8307059c1b667c52741040ce7fce802b9fb31 Mon Sep 17 00:00:00 2001 From: anshggss Date: Sun, 24 May 2026 21:13:32 +0530 Subject: [PATCH 7/7] feat: display username in the navbar on logging in, tracker reloads as username and token are already present, added a profile page to enable logging out --- src/components/Navbar.tsx | 31 +++++-- src/pages/Profile/Profile.tsx | 168 ++++++++++++++++++++++++++++++++++ src/pages/Tracker/Tracker.tsx | 148 +++++++++++++----------------- 3 files changed, 253 insertions(+), 94 deletions(-) create mode 100644 src/pages/Profile/Profile.tsx diff --git a/src/components/Navbar.tsx b/src/components/Navbar.tsx index fd5eac86..0eb29395 100644 --- a/src/components/Navbar.tsx +++ b/src/components/Navbar.tsx @@ -1,16 +1,19 @@ import { NavLink, Link } from "react-router-dom"; import { useState, useContext } from "react"; import { ThemeContext } from "../context/ThemeContext"; +import { UserContext } from "../context/UserContext"; import { Moon, Sun, Menu, X, Github } from "lucide-react"; const Navbar: React.FC = () => { const [isOpen, setIsOpen] = useState(false); const themeContext = useContext(ThemeContext); + const userContext = useContext(UserContext); if (!themeContext) return null; const { toggleTheme, mode } = themeContext; + const user = userContext?.user ?? null; const navLinkStyles = ({ isActive }: { isActive: boolean }) => `px-4 py-2 rounded-xl text-sm lg:text-base font-semibold transition-all duration-300 ${ @@ -53,9 +56,15 @@ const Navbar: React.FC = () => { Contributors - - Login - + {user ? ( + + {user.username} + + ) : ( + + Login + + )} {/* Theme Toggle */}
)} diff --git a/src/pages/Profile/Profile.tsx b/src/pages/Profile/Profile.tsx new file mode 100644 index 00000000..3b18249a --- /dev/null +++ b/src/pages/Profile/Profile.tsx @@ -0,0 +1,168 @@ +import React, { useState, useContext } from "react"; +import axios from "axios"; +import { useNavigate } from "react-router-dom"; +import { ThemeContext } from "../../context/ThemeContext"; +import type { ThemeContextType } from "../../context/ThemeContext"; +import { useUser } from "../../context/UserContext"; +import { KeyIcon, UserIcon, CheckCircle, Eye, EyeOff, LogOut } from "lucide-react"; + +const backendUrl = import.meta.env.VITE_BACKEND_URL; + +const Profile: React.FC = () => { + const themeContext = useContext(ThemeContext) as ThemeContextType; + const { mode } = themeContext; + const { user, updateToken, setUser } = useUser(); + const navigate = useNavigate(); + + const [token, setToken] = useState(user?.token ?? ""); + const [isLoading, setIsLoading] = useState(false); + const [saved, setSaved] = useState(false); + const [error, setError] = useState(""); + const [showToken, setShowToken] = useState(false); + + const handleLogout = async () => { + try { + await axios.get(`${backendUrl}/api/auth/logout`, { withCredentials: true }); + } finally { + setUser(null); + navigate("/login"); + } + }; + + const handleSaveToken = async (e: React.FormEvent) => { + e.preventDefault(); + setIsLoading(true); + setError(""); + setSaved(false); + try { + const response = await axios.post( + `${backendUrl}/api/auth/token`, + { token }, + { withCredentials: true } + ); + if (response.data.success) { + updateToken(token); + setSaved(true); + setTimeout(() => setSaved(false), 3000); + } + } catch { + setError("Failed to save token."); + } finally { + setIsLoading(false); + } + }; + + if (!user) { + return ( +
+

Please log in to view your profile.

+
+ ); + } + + const inputClass = `w-full pl-4 pr-4 py-3 rounded-xl focus:outline-none transition-all ${ + mode === "dark" + ? "bg-white/5 border border-white/10 text-white placeholder-slate-400 focus:ring-2 focus:ring-purple-500" + : "bg-gray-100 border border-gray-200 text-gray-900 placeholder-gray-500 focus:ring-2 focus:ring-purple-400" + }`; + + return ( +
+

+ Profile +

+ + {/* Username */} +
+
+ + Username +
+ +
+ + {/* Logout */} +
+ +
+ + {/* GitHub Token */} +
+
+ + GitHub Personal Access Token +
+
+
+ setToken(e.target.value)} + className={`${inputClass} pr-12`} + /> + +
+

+ + Generate a token + + {" "}with repo and read:user scopes. +

+ + {error && ( +

{error}

+ )} + + +
+
+
+ ); +}; + +export default Profile; diff --git a/src/pages/Tracker/Tracker.tsx b/src/pages/Tracker/Tracker.tsx index 576f39bf..b7d010b2 100644 --- a/src/pages/Tracker/Tracker.tsx +++ b/src/pages/Tracker/Tracker.tsx @@ -1,4 +1,4 @@ -import React, { useState, useEffect } from "react" +import React, { useState, useEffect, useContext } from "react" import { IssueOpenedIcon, IssueClosedIcon, @@ -10,7 +10,6 @@ import { Container, Box, TextField, - Button, Paper, Table, TableBody, @@ -32,6 +31,8 @@ import { import { useTheme } from "@mui/material/styles"; import { useGitHubAuth } from "../../hooks/useGitHubAuth"; import { useGitHubData } from "../../hooks/useGitHubData"; +import { useDebounce } from "../../hooks/useDebounce"; +import { UserContext } from "../../context/UserContext"; import { KeyIcon } from "lucide-react"; const ROWS_PER_PAGE = 10; @@ -49,13 +50,13 @@ interface GitHubItem { const Home: React.FC = () => { const theme = useTheme(); + const userContext = useContext(UserContext); const { username, setUsername, token, setToken, - error: authError, getOctokit, } = useGitHubAuth(); @@ -79,20 +80,32 @@ const Home: React.FC = () => { const [startDate, setStartDate] = useState(""); const [endDate, setEndDate] = useState(""); - // Fetch data when username, tab, or page changes + // Prefill from user context on mount + useEffect(() => { + const user = userContext?.user; + if (user?.username) setUsername(user.username); + if (user?.token) setToken(user.token); + }, []); + + const debouncedUsername = useDebounce(username, 800); + + // Auto-fetch when debounced username or token changes + useEffect(() => { + if (debouncedUsername) { + setPage(0); + fetchData(debouncedUsername, 1, ROWS_PER_PAGE); + } + }, [debouncedUsername, token]); + + // Fetch when tab or page changes (username already set) useEffect(() => { if (username) { fetchData(username, page + 1, ROWS_PER_PAGE); } }, [tab, page]); - const handleSubmit = (e: React.FormEvent): void => { - e.preventDefault(); - setPage(0); - fetchData(username, 1, ROWS_PER_PAGE); - }; - const handlePageChange = (_: unknown, newPage: number) => { + setPage(newPage); }; @@ -167,79 +180,46 @@ const Home: React.FC = () => { {/* Auth Form */} -
- - setUsername(e.target.value)} - required - sx={{ flex: 1, minWidth: 150 }} - /> - setToken(e.target.value)} - type="password" - required - sx={{ flex: 1, minWidth: 150 }} - helperText={ - + setUsername(e.target.value)} + sx={{ flex: 1, minWidth: 150 }} + /> + setToken(e.target.value)} + type="password" + sx={{ flex: 1, minWidth: 150 }} + helperText={ + + - - - Generate new token - - - - • - - - - Learn more - - - } - /> - - - + + Generate new token + + + + Learn more + +
+ } + /> +
{/* Filters */} @@ -324,9 +304,9 @@ const Home: React.FC = () => { - {(authError || dataError) && ( + {dataError && ( - {authError || dataError} + {dataError} )}