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/.env.example
Original file line number Diff line number Diff line change
Expand Up @@ -31,4 +31,5 @@ RAZORPAY_KEY_SECRET=your_razorpay_key_secret
# linkedin
LINKEDIN_CLIENT_ID=your_linkedin_clientId
LINKEDIN_CLIENT_SECRET=your_linkedin_client_secret
LINKEDIN_REDIRECT_URI=http://localhost:5000/api/auth/linkedin/callback
LINKEDIN_REDIRECT_URI=http://localhost:5000/api/auth/linkedin/callback
GEMINI_API_KEY=dummy-key-for-local-dev
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

Remove duplicate GEMINI_API_KEY definition.

GEMINI_API_KEY is already defined on line 8. Having duplicate keys in environment files causes the last occurrence to override earlier ones, leading to configuration confusion. Developers may set the value on line 8 expecting it to work, unaware that line 35 silently overrides it.

🔧 Proposed fix

If the intent is to provide a dummy value for local development, update line 8 instead:

-GEMINI_API_KEY=your_gemini_api_key
+GEMINI_API_KEY=dummy-key-for-local-dev

And remove line 35:

 LINKEDIN_REDIRECT_URI=http://localhost:5000/api/auth/linkedin/callback
-GEMINI_API_KEY=dummy-key-for-local-dev
🧰 Tools
🪛 dotenv-linter (4.0.0)

[warning] 35-35: [DuplicatedKey] The GEMINI_API_KEY key is duplicated

(DuplicatedKey)


[warning] 35-35: [UnorderedKey] The GEMINI_API_KEY key should go before the LINKEDIN_CLIENT_ID key

(UnorderedKey)

🤖 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/.env.example` at line 35, Remove the duplicate GEMINI_API_KEY entry:
delete the second definition of GEMINI_API_KEY (the occurrence shown in the
diff) so there is only one key in the file; if the intent was to provide a
dummy/local value, replace the existing GEMINI_API_KEY value in the first
occurrence instead of adding a second line, ensuring only a single
GEMINI_API_KEY is present.

55 changes: 55 additions & 0 deletions backend/src/middleware/paginate.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
import { asyncHandler } from './errorHandler.js';

/**
* Pagination middleware
* Reads page, limit, sort from query params
* Attaches pagination helpers to req.paginate
* Usage: router.get('/', verifyToken, paginate(), asyncHandler(...))
*/
export const paginate = () =>
asyncHandler(async (req, res, next) => {
// Parse page (default 1, minimum 1)
const page = Math.max(1, parseInt(req.query.page) || 1);

// Parse limit (default 10, max 100)
const limit = Math.min(100, Math.max(1, parseInt(req.query.limit) || 10));

// Parse sort field and direction
// Usage: ?sort=createdAt or ?sort=-createdAt (minus = descending)
const sortParam = req.query.sort || '-createdAt';
const sortOrder = sortParam.startsWith('-') ? -1 : 1;
const sortField = sortParam.replace(/^-/, '');

Comment on lines +19 to +22
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

🌐 Web query:

In Express 4.x, what type is req.query.sortwhen a query string repeats the same key (e.g.,?sort=-createdAt&sort=title)?

💡 Result:

In Express 4.x, when a query string contains repeated keys (e.g.,?sort=-createdAt&sort=title), the type of req.query.sort is typically an Array of strings [1][2][3]. This behavior is provided by the default "extended" query parser, which uses the qs library under the hood [1][4]. Because req.query can contain strings, arrays, or nested objects depending on the input, it is a best practice to verify the type of the property before accessing it (e.g., checking if it is an array) to avoid runtime errors [4]. Important Version Note: In Express version 4.22.0 and 4.22.1, a regression occurred where the qs library's default arrayLimit of 20 caused repeated keys to be parsed as an object (e.g., { "0": "value1", "1": "value2" }) instead of an array when more than 20 values were provided [1][5]. This was resolved in Express 4.22.2, which restored the expected behavior of returning an array for repeated keys regardless of the count [1][5].

Citations:


🏁 Script executed:

# Check if the file exists and read the relevant section
ls -la backend/src/middleware/paginate.js 2>&1 || echo "File not found"

Repository: anurag3407/career-pilot

Length of output: 151


🏁 Script executed:

# Read lines around 19-22 to verify the actual code
head -30 backend/src/middleware/paginate.js | cat -n

Repository: anurag3407/career-pilot

Length of output: 1278


🏁 Script executed:

# Check if similar issues exist with page and limit parameters
cat -n backend/src/middleware/paginate.js | sed -n '11,16p'

Repository: anurag3407/career-pilot

Length of output: 333


Harden sort parsing against non-string query values.

Line 20 can throw when req.query.sort is an array (e.g., repeated sort params like ?sort=-createdAt&sort=title), because startsWith() is only available on strings.

Proposed fix
-    const sortParam = req.query.sort || '-createdAt';
-    const sortOrder = sortParam.startsWith('-') ? -1 : 1;
-    const sortField = sortParam.replace(/^-/, '');
+    const rawSort = Array.isArray(req.query.sort) ? req.query.sort[0] : req.query.sort;
+    const sortParam = typeof rawSort === 'string' && rawSort.trim() ? rawSort.trim() : '-createdAt';
+    const sortOrder = sortParam.startsWith('-') ? -1 : 1;
+    const sortField = sortParam.replace(/^-/, '') || 'createdAt';
🤖 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/src/middleware/paginate.js` around lines 19 - 22, The sort parsing
assumes req.query.sort is a string; if it's an array (repeated params)
startsWith will throw — fix by normalizing sortParam first (e.g., const rawSort
= req.query.sort; const sortParam = Array.isArray(rawSort) ? rawSort[0] :
(rawSort || '-createdAt')); then compute sortOrder and sortField from that
normalized sortParam used by the existing sortOrder and sortField logic in
paginate middleware so startsWith and replace are only called on a string.

const skip = (page - 1) * limit;

// Attach to req so route handlers can use it
req.paginate = {
page,
limit,
skip,
sort: { [sortField]: sortOrder }
};

next();
});

/**
* Helper to build consistent paginated response
* Call this inside your route handler to send the response
*/
export const paginatedResponse = (res, { data, total, page, limit }) => {
const totalPages = Math.ceil(total / limit);

res.json({
success: true,
data,
meta: {
total,
page,
limit,
totalPages,
hasNextPage: page < totalPages,
hasPrevPage: page > 1
}
});
};
24 changes: 11 additions & 13 deletions backend/src/routes/resume.js
Original file line number Diff line number Diff line change
@@ -1,33 +1,31 @@
import express from 'express';
import { verifyToken } from '../middleware/auth.js';
import { asyncHandler, ApiError } from '../middleware/errorHandler.js';
import { paginate, paginatedResponse } from '../middleware/paginate.js';
import Resume from '../models/Resume.model.js';

const router = express.Router();

// Get all resumes for a user
router.get('/', verifyToken, asyncHandler(async (req, res) => {
// Get all resumes for a user (paginated)
router.get('/', verifyToken, paginate(), asyncHandler(async (req, res) => {
const userId = req.user.uid;

// Get resumes from MongoDB sorted by creation date (newest first)
const { page, limit, skip, sort } = req.paginate;

const total = await Resume.countDocuments({ userId });

const userResumes = await Resume.find({ userId })
.sort({ createdAt: -1 })
.sort(sort)
.skip(skip)
.limit(limit)
.lean();

// Transform _id to id for frontend compatibility
const resumes = userResumes.map(resume => ({
id: resume._id.toString(),
...resume,
_id: undefined
}));

res.json({
success: true,
data: {
resumes,
count: resumes.length
}
});
paginatedResponse(res, { data: resumes, total, page, limit });
}));

// Get a specific resume
Expand Down