Skip to content
Open
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
1 change: 0 additions & 1 deletion .env.example

This file was deleted.

61 changes: 54 additions & 7 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,12 +1,13 @@
# 🌟 **GitHub Tracker** 🌟

<!-- top -->

**Track Activity of Users on GitHub**

Welcome to **GitHub Tracker**, a web app designed to help you monitor and analyze the activity of GitHub users. Whether you’re a developer, a project manager, or just curious, this tool simplifies tracking contributions and activity across repositories! 🚀👩‍💻

<p align="center">
<img src="public/crl.png" height="60px" alt="github-tracker">
<img src="frontend/public/crl.png" height="60px" alt="github-tracker">
</p>
<table align="center">
<thead align="center">
Expand Down Expand Up @@ -41,47 +42,93 @@ Welcome to **GitHub Tracker**, a web app designed to help you monitor and analyz
---

## 🚀 Setup Guide

1. Clone the repository to your local machine:

```bash
$ git clone https://github.com/yourusername/github-tracker.git
```

2. Navigate to the project directory:

```bash
$ cd github-tracker
```

3. Run the frontend

```bash
$ cd frontend
$ npm i
$ npm run dev
```

4. Run the backend

```bash
$ cd backend
$ npm i
$ npm start
```

## 🔐 MongoDB & Authentication Setup

The application uses MongoDB for user authentication (Sign up, Login, Logout).

### Frontend Environment Setup

Create a `.env` file in the `frontend/` folder:

```
VITE_BACKEND_URL=http://localhost:5000
```

See `frontend/.env.example` for reference.

### Backend Environment Setup

Create a `.env` file in the `backend/` folder:

```
MONGO_URI=mongodb+srv://alokhacs222729_db_user:IvORbhhzQg71OF2q@cluster0.gevcwty.mongodb.net/?appName=Cluster0
PORT=5000
SESSION_SECRET=your_session_secret_key_here
```
Comment on lines +92 to +96
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 | 🔴 Critical | ⚡ Quick win

Remove hardcoded MongoDB credentials from documentation immediately.

Line 93 exposes a full connection string with embedded credentials. This must be replaced with placeholders, and the exposed credentials should be rotated/revoked.

🔒 Proposed fix
-MONGO_URI=mongodb+srv://alokhacs222729_db_user:IvORbhhzQg71OF2q@cluster0.gevcwty.mongodb.net/?appName=Cluster0
+MONGO_URI=mongodb+srv://<db_user>:<db_password>@cluster0.gevcwty.mongodb.net/<db_name>?retryWrites=true&w=majority
 PORT=5000
 SESSION_SECRET=your_session_secret_key_here
🧰 Tools
🪛 Betterleaks (1.2.0)

[high] 93-93: Detected a MongoDB connection string with embedded credentials, potentially exposing direct database access and sensitive application data.

(mongodb-connection-string)

🪛 markdownlint-cli2 (0.22.1)

[warning] 92-92: Fenced code blocks should have a language specified

(MD040, fenced-code-language)

🤖 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 `@README.md` around lines 92 - 96, The README currently contains a hardcoded
MongoDB connection string (MONGO_URI) with embedded credentials; replace that
value with a placeholder (e.g.,
MONGO_URI=mongodb+srv://<DB_USER>:<DB_PASS>@<HOST>/<DB_NAME>?retryWrites=true&w=majority)
and update the example block so no real secrets appear, and add a short note to
rotate/revoke the exposed credentials immediately; locate the example using the
MONGO_URI token and update it accordingly.


See `backend/.env.example` for reference.

**Key Points:**

- The `MONGO_URI` connects to MongoDB Atlas for user database storage
- Users can sign up with email and password
- Passwords are securely hashed using bcryptjs
- Sessions are maintained via express-session and Passport.js
- Both frontend and backend are required for authentication to work

## 🧪 Backend Unit & Integration Testing with Jasmine

This project uses the Jasmine framework for backend unit and integration tests. The tests cover:

- User model (password hashing, schema, password comparison)
- Authentication routes (signup, login, logout)
- Passport authentication logic (via integration tests)

### Prerequisites

- **Node.js** and **npm** installed
- **MongoDB** running locally (default: `mongodb://127.0.0.1:27017`)

### Installation

Install all required dependencies:

```sh
npm install
npm install --save-dev jasmine @types/jasmine supertest express-session passport passport-local bcryptjs
```

### Running the Tests

1. **Start MongoDB** (if not already running):
```sh
mongod
Expand All @@ -92,24 +139,26 @@ npm install --save-dev jasmine @types/jasmine supertest express-session passport
```

### Test Files

- `spec/user.model.spec.cjs` — Unit tests for the User model
- `spec/auth.routes.spec.cjs` — Integration tests for authentication routes

### Jasmine Configuration

The Jasmine config (`spec/support/jasmine.mjs`) is set to recognize `.cjs`, `.js`, and `.mjs` test files:

```js
spec_files: [
"**/*[sS]pec.?(m)js",
"**/*[sS]pec.cjs"
]
spec_files: ["**/*[sS]pec.?(m)js", "**/*[sS]pec.cjs"];
```

### Troubleshooting

- **No specs found:** Ensure your test files have the correct extension and are in the `spec/` directory.
- **MongoDB connection errors:** Make sure MongoDB is running and accessible.
- **Missing modules:** Install any missing dev dependencies with `npm install --save-dev <module>`.

### What Was Covered

- Jasmine is set up and configured for backend testing.
- All major backend modules are covered by unit/integration tests.
- Tests are passing and verified.
Expand All @@ -131,8 +180,6 @@ spec_files: [
</a>
</div>



---

<p align="center">
Expand Down
3 changes: 0 additions & 3 deletions backend/.env.sample

This file was deleted.

8 changes: 8 additions & 0 deletions backend/models/User.js
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,14 @@ const UserSchema = new mongoose.Schema({
type: String,
required: true,
},
trackerHistory: {
type: Array,
default: [],
},
lastTrackedAt: {
type: Date,
default: Date.now,
},
});

UserSchema.pre('save', async function (next) {
Expand Down
44 changes: 44 additions & 0 deletions backend/routes/auth.js
Original file line number Diff line number Diff line change
Expand Up @@ -39,4 +39,48 @@ router.get("/logout", (req, res) => {
});
});

// Get user tracker history
router.get("/tracker-history", async (req, res) => {
if (!req.user) {
return res.status(401).json({ message: 'Not authenticated' });
}
try {
const user = await User.findById(req.user.id).select('trackerHistory');
res.status(200).json({ trackerHistory: user?.trackerHistory || [] });
} catch (err) {
res.status(500).json({ message: 'Error fetching tracker history', error: err.message });
}
});

// Save tracker search to history
router.post("/tracker-history", async (req, res) => {
if (!req.user) {
return res.status(401).json({ message: 'Not authenticated' });
}
const { username, searchedAt } = req.body;
try {
const user = await User.findById(req.user.id);
if (!user) {
return res.status(404).json({ message: 'User not found' });
}

// Remove duplicate if exists (keep only unique searches)
user.trackerHistory = user.trackerHistory.filter(
item => item.username.toLowerCase() !== username.toLowerCase()
);

// Add new search at the beginning
user.trackerHistory.unshift({ username, searchedAt: new Date(searchedAt) });

Comment on lines +60 to +74
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

Validate username and searchedAt before processing history.

The route can throw when username is missing/non-string, and invalid searchedAt is not rejected. This turns client input errors into 500s.

Suggested patch
     const { username, searchedAt } = req.body;
+    if (typeof username !== "string" || !username.trim()) {
+        return res.status(400).json({ message: "username is required" });
+    }
+    const parsedSearchedAt = searchedAt ? new Date(searchedAt) : new Date();
+    if (Number.isNaN(parsedSearchedAt.getTime())) {
+        return res.status(400).json({ message: "searchedAt must be a valid date" });
+    }
     try {
         const user = await User.findById(req.user.id);
         if (!user) {
             return res.status(404).json({ message: 'User not found' });
         }
         
         // Remove duplicate if exists (keep only unique searches)
         user.trackerHistory = user.trackerHistory.filter(
-            item => item.username.toLowerCase() !== username.toLowerCase()
+            item => item.username?.toLowerCase() !== username.toLowerCase()
         );
         
         // Add new search at the beginning
-        user.trackerHistory.unshift({ username, searchedAt: new Date(searchedAt) });
+        user.trackerHistory.unshift({ username, searchedAt: parsedSearchedAt });
📝 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 { username, searchedAt } = req.body;
try {
const user = await User.findById(req.user.id);
if (!user) {
return res.status(404).json({ message: 'User not found' });
}
// Remove duplicate if exists (keep only unique searches)
user.trackerHistory = user.trackerHistory.filter(
item => item.username.toLowerCase() !== username.toLowerCase()
);
// Add new search at the beginning
user.trackerHistory.unshift({ username, searchedAt: new Date(searchedAt) });
const { username, searchedAt } = req.body;
if (typeof username !== "string" || !username.trim()) {
return res.status(400).json({ message: "username is required" });
}
const parsedSearchedAt = searchedAt ? new Date(searchedAt) : new Date();
if (Number.isNaN(parsedSearchedAt.getTime())) {
return res.status(400).json({ message: "searchedAt must be a valid date" });
}
try {
const user = await User.findById(req.user.id);
if (!user) {
return res.status(404).json({ message: 'User not found' });
}
// Remove duplicate if exists (keep only unique searches)
user.trackerHistory = user.trackerHistory.filter(
item => item.username?.toLowerCase() !== username.toLowerCase()
);
// Add new search at the beginning
user.trackerHistory.unshift({ username, searchedAt: parsedSearchedAt });
🤖 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 60 - 74, Validate and sanitize
req.body.username and req.body.searchedAt before using them: ensure username
exists and is a non-empty string, and ensure searchedAt is a valid date (or
omit/replace with Date.now() if invalid). In the route where you fetch User
(User.findById and then mutate user.trackerHistory), add checks that username is
typeof string and trim() !== "" and that new Date(searchedAt) is valid
(isNaN(Date.parse(...)) check) and return a 400 with a clear message if
validation fails; only then perform the existing trackerHistory filtering
(user.trackerHistory.filter(...)) and unshift with a normalized { username:
username.trim(), searchedAt: new Date(validDate) } entry.

// Keep only last 10 searches
user.trackerHistory = user.trackerHistory.slice(0, 10);
user.lastTrackedAt = new Date();

await user.save();
res.status(200).json({ message: 'Tracker history saved', trackerHistory: user.trackerHistory });
} catch (err) {
res.status(500).json({ message: 'Error saving tracker history', error: err.message });
}
});

module.exports = router;
10 changes: 9 additions & 1 deletion backend/server.js
Original file line number Diff line number Diff line change
Expand Up @@ -12,14 +12,22 @@ require('./config/passportConfig');
const app = express();

// CORS configuration
app.use(cors('*'));
app.use(cors({
origin: process.env.FRONTEND_URL || 'http://localhost:5173',
credentials: true
}));

// Middleware
app.use(bodyParser.json());
app.use(session({
secret: process.env.SESSION_SECRET,
resave: false,
saveUninitialized: false,
cookie: {
httpOnly: true,
secure: false, // set to true in production with HTTPS
sameSite: 'lax'
}
Comment on lines +26 to +30
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

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Verify current session/proxy configuration in backend
rg -n "app\\.set\\(['\"]trust proxy['\"]|session\\(|secure:|sameSite:" backend/server.js
rg -n "NODE_ENV|FRONTEND_URL|SESSION_SECRET" backend/.env .env 2>/dev/null || true

Repository: GitMetricsLab/github_tracker

Length of output: 218


Add environment-aware session cookie security and trust proxy configuration.

The session cookie has secure: false hardcoded (line 28), allowing production sessions to transmit over non-HTTPS. Additionally, app.set('trust proxy', 1) is missing, which breaks session handling when deployed behind a reverse proxy.

Set secure based on the environment and configure proxy trust. For cross-origin scenarios (FRONTEND_URL with credentials), also adjust sameSite to 'none' in production.

Suggested patch
+app.set('trust proxy', 1);
+
 app.use(session({
     secret: process.env.SESSION_SECRET,
     resave: false,
     saveUninitialized: false,
     cookie: { 
         httpOnly: true, 
-        secure: false, // set to true in production with HTTPS
-        sameSite: 'lax'
+        secure: process.env.NODE_ENV === 'production',
+        sameSite: process.env.NODE_ENV === 'production' ? 'none' : 'lax'
     }
 }));
🤖 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/server.js` around lines 26 - 30, Update the session cookie and proxy
settings to be environment-aware: call app.set('trust proxy', 1) during server
setup, and in the session middleware's cookie config (the cookie object
currently containing httpOnly/secure/sameSite) set secure =
(process.env.NODE_ENV === 'production') and set sameSite = 'none' when in
production and FRONTEND_URL is cross-origin (otherwise keep 'lax'); ensure
secure is true in production so cookies require HTTPS and adjust sameSite to
'none' when sending credentials across origins. Use the existing session
middleware cookie object and the app.set('trust proxy', 1) symbol names to
locate where to apply these changes.

}));
app.use(passport.initialize());
app.use(passport.session());
Expand Down
6 changes: 3 additions & 3 deletions docker-compose.yml
Original file line number Diff line number Diff line change
Expand Up @@ -3,14 +3,14 @@ services:
image: frontend
container_name: frontend-container
build:
context: .
context: ./frontend
dockerfile: Dockerfile.dev
ports:
- "5173:5173"
env_file:
- .env
volumes:
- .:/app
- ./frontend:/app
- /app/node_modules
depends_on:
- backend
Expand Down Expand Up @@ -39,7 +39,7 @@ services:
image: frontend-prod
container_name: frontend-prod-container
build:
context: .
context: ./frontend
dockerfile: Dockerfile.prod
ports:
- "3000:3000"
Expand Down
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
4 changes: 4 additions & 0 deletions package.json → frontend/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
"dev": "vite --host",
"build": "vite build",
"lint": "eslint .",
"test": "jasmine",
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

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Find all Jasmine spec files
fd -e cjs -e js "spec\." 

# Check jasmine configuration
fd -t f "jasmine.json"

Repository: GitMetricsLab/github_tracker

Length of output: 123


🏁 Script executed:

#!/bin/bash
# Check repository structure
echo "=== Repository structure ===" 
ls -la | head -30

# Check if there are separate frontend and backend directories
echo -e "\n=== Checking for frontend/backend directories ==="
[ -d "frontend" ] && echo "frontend/ exists" || echo "frontend/ does NOT exist"
[ -d "backend" ] && echo "backend/ exists" || echo "backend/ does NOT exist"

# Check location of spec files - are they at root or in backend?
echo -e "\n=== Full paths of spec files ==="
find . -name "*.spec.cjs" -o -name "*.spec.js" 2>/dev/null | head -20

# Read frontend/package.json if it exists
echo -e "\n=== frontend/package.json (lines 1-20) ==="
[ -f "frontend/package.json" ] && head -20 frontend/package.json || echo "frontend/package.json not found"

# Read backend/package.json if it exists
echo -e "\n=== backend/package.json (lines 1-20) ==="
[ -f "backend/package.json" ] && head -20 backend/package.json || echo "backend/package.json not found"

# Check if spec directory exists at root
echo -e "\n=== Check spec/ directory ==="
[ -d "spec" ] && echo "spec/ exists at root" && ls -la spec/ || echo "spec/ does NOT exist at root"

Repository: GitMetricsLab/github_tracker

Length of output: 2655


🏁 Script executed:

#!/bin/bash
# Check for root-level package.json
echo "=== Root-level package.json ==="
[ -f "package.json" ] && cat package.json || echo "No package.json at root"

# Check for jasmine configuration files
echo -e "\n=== Jasmine configuration files ==="
fd -t f -i "jasmine" 2>/dev/null | head -20

# Check if frontend has jasmine config
echo -e "\n=== Frontend directory contents ==="
ls -la frontend/ | grep -E "(jasmine|spec|test)"

# Check if there's a jasmine.json or similar
echo -e "\n=== Looking for test config files ==="
find . -maxdepth 2 -type f \( -name "*jasmine*" -o -name "*.spec.js" -o -name "*.spec.cjs" -o -name "jest.config.*" \) 2>/dev/null

Repository: GitMetricsLab/github_tracker

Length of output: 321


🏁 Script executed:

#!/bin/bash
# Read jasmine configuration
echo "=== spec/support/jasmine.mjs ==="
cat spec/support/jasmine.mjs

# Check what dependencies are in frontend vs backend for jasmine
echo -e "\n=== Jasmine in frontend/package.json dependencies ==="
grep -i "jasmine" frontend/package.json || echo "No jasmine in frontend dependencies"

echo -e "\n=== Jasmine in backend/package.json dependencies ==="
grep -i "jasmine" backend/package.json || echo "No jasmine in backend dependencies"

# Check full devDependencies in both
echo -e "\n=== Frontend devDependencies ==="
grep -A 20 '"devDependencies"' frontend/package.json | head -25

echo -e "\n=== Backend devDependencies ==="
grep -A 20 '"devDependencies"' backend/package.json | head -25

Repository: GitMetricsLab/github_tracker

Length of output: 1379


Move the test script to backend/package.json.

The "test": "jasmine" script in frontend/package.json is configured to run backend tests (spec/auth.routes.spec.cjs, spec/user.model.spec.cjs), not frontend tests. This script and its dependencies should be in backend/package.json to maintain proper separation between frontend and backend. Additionally, the jasmine package and @types/jasmine should be moved to backend's devDependencies, not frontend's.

🤖 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 `@frontend/package.json` at line 10, The frontend package.json currently
defines the "test" script ("test": "jasmine") and lists jasmine and
`@types/jasmine` in devDependencies though those tests are backend specs; remove
the "test" script and jasmine-related devDependencies from frontend/package.json
and add them into backend/package.json instead: add the "test": "jasmine" script
to backend/package.json and move jasmine and `@types/jasmine` into backend's
devDependencies so backend runs the Jasmine specs (refer to the "test" script
and the jasmine/@types/jasmine entries when updating the two package.json
files).

"preview": "vite preview",
"docker:dev": "docker compose --profile dev up --build",
"docker:prod": "docker compose --profile prod up -d --build"
Expand Down Expand Up @@ -45,12 +46,15 @@
"eslint-plugin-react": "^7.37.2",
"eslint-plugin-react-hooks": "^5.0.0",
"eslint-plugin-react-refresh": "^0.4.14",
"express": "^5.2.1",
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

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Check if backend has its own package.json
fd -t f "package.json" backend/

# If it exists, check its dependencies
if [ -f backend/package.json ]; then
  echo "=== backend/package.json dependencies ==="
  cat backend/package.json | jq '.dependencies, .devDependencies'
fi

Repository: GitMetricsLab/github_tracker

Length of output: 393


🏁 Script executed:

cat -n frontend/package.json

Repository: GitMetricsLab/github_tracker

Length of output: 2227


Remove backend dependencies from frontend package.json devDependencies.

The following backend runtime dependencies are misplaced in frontend/package.json and should not appear there at all: express, mongoose, express-session, passport, passport-local, bcryptjs, and supertest. These packages already exist in backend/package.json where they belong. Their presence in the frontend package.json will:

  • Unnecessarily bloat the frontend bundle
  • Create confusion in dependency management
  • Violate the separation of concerns established by the project structure

Remove lines 49, 50, 53-55 (and bcryptjs on line 44 and supertest on line 56) from devDependencies.

🤖 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 `@frontend/package.json` at line 49, Remove the backend runtime packages
mistakenly declared in frontend's devDependencies: delete the entries for
"express", "mongoose", "express-session", "passport", "passport-local",
"bcryptjs", and "supertest" from frontend/package.json so only frontend-related
packages remain; locate the devDependencies block in frontend/package.json and
remove those package keys (ensure you don't touch backend/package.json where
these belong) and then run npm/yarn install to update lockfile.

"express-session": "^1.18.2",
"globals": "^15.11.0",
"jasmine": "^5.9.0",
"mongoose": "^9.6.2",
"passport": "^0.7.0",
"passport-local": "^1.0.0",
"supertest": "^7.1.4",
"typescript-eslint": "^8.59.2",
"vite": "^5.4.10"
}
}
File renamed without changes.
File renamed without changes.
File renamed without changes
File renamed without changes
File renamed without changes
File renamed without changes.
4 changes: 2 additions & 2 deletions src/App.tsx → frontend/src/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -8,12 +8,12 @@ import ThemeWrapper from "./context/ThemeContext";
function App() {
return (
<ThemeWrapper>
<div className="relative flex flex-col min-h-screen">
<div className="relative flex flex-col min-h-screen w-full">
<ScrollProgressBar />

<Navbar />

<main className="flex-grow bg-gray-50 dark:bg-gray-800 flex justify-center items-center">
<main className="flex-grow bg-gray-50 dark:bg-gray-800 w-full">
<Router />
</main>

Expand Down
File renamed without changes.
File renamed without changes
Loading
Loading