Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
3 changes: 2 additions & 1 deletion backend/config/passportConfig.js
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down
20 changes: 11 additions & 9 deletions backend/models/User.js
Original file line number Diff line number Diff line change
Expand Up @@ -16,18 +16,19 @@ const UserSchema = new mongoose.Schema({
type: String,
required: true,
},
token: {
type: String,
unique: true,
sparse: true,
},
Comment on lines +19 to +23
Comment on lines +19 to +23
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major | 🏗️ Heavy lift

Avoid storing GitHub PATs as plaintext in user records.

This persists a long-lived credential directly in clear text. Encrypt it at rest (or use a secrets vault pattern) and avoid exposing it by default in queries.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@backend/models/User.js` around lines 19 - 23, The User schema currently
stores the GitHub PAT in the token field as plaintext; change storage to a
secure pattern: stop exposing it by default (make the token field
non-selectable) and switch to encrypted-at-rest or a secrets-vault reference.
Concretely, update the token definition in User schema (token) to set it as
excluded from default queries (select:false) and replace direct assignment with
a setter/getter that encrypts/decrypts using the chosen key management solution
(or store only a vault/secretId reference instead of the raw PAT); ensure any
code paths that read/write token use the new encrypt/decrypt or vault API
through well-named helpers (e.g., UserSchema.methods.setGithubToken /
getGithubToken) so plaintext tokens are never persisted or returned by default.

});

// ✅ FIXED: no next()
UserSchema.pre('save', async function (next) {
if (!this.isModified('password')) return next();
try {
const salt = await bcrypt.genSalt(10);
this.password = await bcrypt.hash(this.password, salt);
next(); // Tells Mongoose hashing is done, save the document now
} catch (err) {
next(err); // Safely passes any encryption errors to the database handler
}
UserSchema.pre("save", async function () {
if (!this.isModified("password")) return;

const salt = await bcrypt.genSalt(10);
this.password = await bcrypt.hash(this.password, salt);
});

// ✅ password comparison
Expand All @@ -36,3 +37,4 @@ UserSchema.methods.comparePassword = async function (enteredPassword) {
};

module.exports = mongoose.model("User", UserSchema);

18 changes: 18 additions & 0 deletions backend/routes/auth.js
Original file line number Diff line number Diff line change
Expand Up @@ -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' });
}
Comment on lines +43 to +46
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Reject whitespace-only token input.

At Line 44, a string like " " passes validation and gets stored as a token. Trim first and validate the trimmed value.

Suggested patch
-    const { token } = req.body;
-    if (!token) {
+    const token = req.body.token?.trim();
+    if (!token) {
         return res.status(400).json({ message: 'Token is required' });
     }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
const { token } = req.body;
if (!token) {
return res.status(400).json({ message: 'Token is required' });
}
const token = req.body.token?.trim();
if (!token) {
return res.status(400).json({ message: 'Token is required' });
}
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@backend/routes/auth.js` around lines 43 - 46, The current check accepts
whitespace-only tokens; update the handler that reads const { token } = req.body
to trim the incoming string and validate the trimmed value instead: create a
trimmedToken (e.g., tokenTrimmed = token && token.trim()), return
res.status(400).json({ message: 'Token is required' }) when tokenTrimmed is
falsy/empty, and thereafter use the trimmedToken wherever the token is stored or
used (replace usages of token with the trimmed value).

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 });
}
Comment on lines +39 to +53
Comment on lines +48 to +53
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Verify update success and avoid leaking raw backend errors.

At Line 48-53, this always returns success when no document is updated, and exposes err.message to clients. Return a safe generic 500 and handle null-update/duplicate-key cases explicitly.

Suggested patch
-        await User.findByIdAndUpdate(req.user._id, { token });
+        const updated = await User.findByIdAndUpdate(
+          req.user._id,
+          { token },
+          { new: true, runValidators: true }
+        );
+        if (!updated) {
+          return res.status(404).json({ message: "User not found" });
+        }
         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 });
+        if (err?.code === 11000) {
+          return res.status(409).json({ message: "Token already in use" });
+        }
+        res.status(500).json({ message: 'Error saving token' });
     }
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@backend/routes/auth.js` around lines 48 - 53, The current token-save logic
always returns success and leaks internal error messages; update the handler
around User.findByIdAndUpdate to check the returned value (result) and only set
req.user.token and send a 200 when result is truthy, return a 404/appropriate
client error if no document was updated, and on exceptions avoid exposing
err.message to clients — log the full error server-side and return a generic 500
message; also detect duplicate-key errors (e.g., err.code === 11000) and return
a 409-ish response with a safe message.

});
Comment on lines +38 to +54

// Logout route
router.get("/logout", (req, res) => {

Expand Down
2 changes: 1 addition & 1 deletion src/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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();
Expand Down
10 changes: 6 additions & 4 deletions src/Routes/Router.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -9,8 +9,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 (
Expand All @@ -26,9 +28,9 @@ const Router = () => {
<Route path="/contributors" element={<Contributors />} />
<Route path="/contributor/:username" element={<ContributorProfile />} />
<Route path="/activity" element={<Activity />} />

{/* Privacy Policy page route */}
<Route path="/privacy" element={<PrivacyPolicy />} />
<Route path="/enterToken" element={<SetToken />} />
<Route path="/profile" element={<Profile />} />
</Routes>
);
};
Expand Down
31 changes: 21 additions & 10 deletions src/components/Navbar.tsx
Original file line number Diff line number Diff line change
@@ -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";
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Remove unused Github import to clear lint failure.

Github is imported but never used, matching the ESLint error.

Suggested fix
-import { Moon, Sun, Menu, X, Github } from "lucide-react";
+import { Moon, Sun, Menu, X } from "lucide-react";
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
import { Moon, Sun, Menu, X, Github } from "lucide-react";
import { Moon, Sun, Menu, X } from "lucide-react";
🧰 Tools
🪛 ESLint

[error] 5-5: 'Github' is defined but never used.

(@typescript-eslint/no-unused-vars)

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/components/Navbar.tsx` at line 5, Remove the unused Github import from
the lucide-react import list in Navbar.tsx: locate the import statement that
currently imports Moon, Sun, Menu, X, Github and delete Github so only the used
icons (Moon, Sun, Menu, X) remain, which will resolve the ESLint unused-import
error.


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 ${
Expand Down Expand Up @@ -53,9 +56,15 @@ const Navbar: React.FC = () => {
Contributors
</NavLink>

<NavLink to="/login" className={navLinkStyles}>
Login
</NavLink>
{user ? (
<NavLink to="/profile" className={navLinkStyles}>
{user.username}
</NavLink>
) : (
<NavLink to="/login" className={navLinkStyles}>
Login
</NavLink>
)}

{/* Theme Toggle */}
<button
Expand Down Expand Up @@ -131,13 +140,15 @@ const Navbar: React.FC = () => {
Contributors
</NavLink>

<NavLink
to="/login"
className={navLinkStyles}
onClick={closeMenu}
>
Login
</NavLink>
{user ? (
<NavLink to="/profile" className={navLinkStyles} onClick={closeMenu}>
{user.username}
</NavLink>
) : (
<NavLink to="/login" className={navLinkStyles} onClick={closeMenu}>
Login
</NavLink>
)}
</div>
</div>
)}
Expand Down
135 changes: 135 additions & 0 deletions src/components/SetToken.tsx
Original file line number Diff line number Diff line change
@@ -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("/");
}
Comment on lines +30 to +33
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Handle non-success API responses explicitly.

If the request resolves but success is false, the UI shows no error and does nothing. Add an else branch to set a message.

Suggested patch
       if (response.data.success) {
         updateToken(token);
         navigate("/");
+      } else {
+        setMessage(response.data.message ?? "Failed to save token.");
       }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
if (response.data.success) {
updateToken(token);
navigate("/");
}
if (response.data.success) {
updateToken(token);
navigate("/");
} else {
setMessage(response.data.message ?? "Failed to save token.");
}
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/components/SetToken.tsx` around lines 30 - 33, The handler in the
SetToken component currently only acts when response.data.success is true; add
an else branch after the if (response.data.success) block to explicitly set an
error message in component state (use the existing message setter, e.g.,
setMessage) and avoid silent failures; prefer using response.data.message if
present, otherwise set a generic failure string—keep calls to updateToken(token)
and navigate("/") only inside the success branch.

} catch {
setMessage("Failed to save token. Make sure you are logged in.");
} finally {
setIsLoading(false);
}
};

return (
<div
className={`min-h-screen h-full w-full flex items-center justify-center relative overflow-hidden ${
mode === "dark"
? "bg-gradient-to-br from-slate-900 via-purple-900 to-slate-900"
: "bg-gradient-to-br from-slate-100 via-purple-100 to-slate-100"
}`}
>
<div className="absolute inset-0">
<div className={`absolute -top-40 -right-40 w-96 h-96 ${mode === "dark" ? "bg-purple-500" : "bg-purple-300"} rounded-full blur-3xl opacity-30 animate-pulse`} />
<div className={`absolute -bottom-40 -left-40 w-96 h-96 ${mode === "dark" ? "bg-blue-500" : "bg-blue-300"} rounded-full blur-3xl opacity-30 animate-pulse`} />
</div>

<div className="relative w-full max-w-md px-4 sm:px-6">
<div className="text-center mb-10">
<div className="inline-flex items-center justify-center w-20 h-20 bg-white rounded-3xl mb-6 shadow-2xl">
<KeyIcon className="w-10 h-10 text-purple-600" />
</div>
<h1 className={`text-3xl font-bold bg-clip-text text-transparent mb-2 ${mode === "dark" ? "bg-gradient-to-r from-purple-300 via-pink-300 to-indigo-300" : "bg-gradient-to-r from-purple-600 via-pink-600 to-indigo-600"}`}>
GitHub Token
</h1>
<p className={`${mode === "dark" ? "text-slate-300" : "text-gray-700"} text-base`}>
Enter your Personal Access Token to get started
</p>
</div>

<div className={`rounded-3xl p-6 sm:p-10 shadow-2xl border ${mode === "dark" ? "bg-white/10 backdrop-blur-xl border-white/20 text-white" : "bg-white border-gray-200 text-black"}`}>
<form onSubmit={handleSubmit} className="space-y-6">
<div>
<div className="relative">
<input
type={showToken ? "text" : "password"}
placeholder="ghp_xxxxxxxxxxxxxxxxxxxx"
value={token}
onChange={(e) => 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"
}`}
/>
<button
type="button"
onClick={() => setShowToken((v) => !v)}
className={`absolute right-3 top-1/2 -translate-y-1/2 p-1 rounded-lg transition-colors ${
mode === "dark" ? "text-slate-400 hover:text-white" : "text-gray-400 hover:text-gray-700"
}`}
aria-label={showToken ? "Hide token" : "Show token"}
>
{showToken ? <EyeOff className="w-5 h-5" /> : <Eye className="w-5 h-5" />}
</button>
</div>
<p className={`mt-2 text-xs ${mode === "dark" ? "text-slate-400" : "text-gray-500"}`}>
<a
href="https://github.com/settings/tokens/new"
target="_blank"
rel="noopener noreferrer"
className="text-purple-400 hover:underline"
>
Generate a new token
</a>
{" "}with <code>repo</code> and <code>read:user</code> scopes.
</p>
</div>

<button
type="submit"
disabled={isLoading}
className="w-full bg-gradient-to-r from-purple-600 via-pink-600 to-indigo-600 text-white py-4 px-6 rounded-2xl font-semibold transition-all duration-300 hover:scale-[1.02] hover:shadow-xl disabled:opacity-50 disabled:cursor-not-allowed"
>
{isLoading ? "Saving..." : "Save Token"}
</button>

<button
type="button"
onClick={() => navigate("/")}
className={`w-full py-3 px-6 rounded-2xl font-medium transition-all ${mode === "dark" ? "text-slate-400 hover:text-white" : "text-gray-500 hover:text-gray-800"}`}
>
Skip for now
</button>
</form>

{message && (
<div className="mt-4 p-4 rounded-2xl text-center text-sm font-medium bg-red-500/20 text-red-300 border border-red-500/30">
{message}
</div>
)}
</div>
</div>
</div>
);
};

export default SetToken;
40 changes: 40 additions & 0 deletions src/context/UserContext.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
import React, { createContext, useContext, useState } from "react";

export interface UserData {
_id: string;
username: string;
email: string;
token?: string;
}
Comment on lines +3 to +8
Comment on lines +3 to +8
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Unify user identifier contract (id vs _id) across backend/frontend.

UserData requires _id, but current auth payload uses id. This mismatch is brittle and can break state handling unless every caller remaps fields.

Suggested patch (frontend contract-side)
 export interface UserData {
-  _id: string;
+  id: string;
+  _id?: string;
   username: string;
   email: string;
   token?: string;
 }
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/context/UserContext.tsx` around lines 3 - 8, UserData currently declares
_id while backend auth payload uses id; update the frontend contract to accept
the backend shape by replacing or supporting _id with id: change the UserData
interface (symbol: UserData in src/context/UserContext.tsx) so it uses id:
string instead of _id, or allow both by adding id?: string and
mapping/normalizing when consuming auth payloads (e.g., normalizeAuthPayload
functions or where setUser is called) so the app consistently uses user.id
internally; ensure all usages of UserData (state, setUser, selectors) read the
normalized id field.


interface UserContextType {
user: UserData | null;
setUser: (user: UserData | null) => void;
updateToken: (token: string) => void;
}

export const UserContext = createContext<UserContextType | null>(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<UserData | null>(null);

const setUser = (u: UserData | null) => setUserState(u);

const updateToken = (token: string) => {
setUserState((prev) => (prev ? { ...prev, token } : prev));
};

return (
<UserContext.Provider value={{ user, setUser, updateToken }}>
{children}
</UserContext.Provider>
);
};

export default UserProvider;
13 changes: 8 additions & 5 deletions src/main.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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(
<StrictMode>
<ThemeWrapper>
<BrowserRouter>
<App />
</BrowserRouter>
</ThemeWrapper>
<UserProvider>
<ThemeWrapper>
<BrowserRouter>
<App />
</BrowserRouter>
</ThemeWrapper>
</UserProvider>
</StrictMode>
);
Loading
Loading