diff --git a/backend/.env.example b/backend/.env.example index 406a41b83..3bfb4a2a6 100644 --- a/backend/.env.example +++ b/backend/.env.example @@ -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 \ No newline at end of file +LINKEDIN_REDIRECT_URI=http://localhost:5000/api/auth/linkedin/callback +GEMINI_API_KEY=dummy-key-for-local-dev diff --git a/backend/src/middleware/paginate.js b/backend/src/middleware/paginate.js new file mode 100644 index 000000000..d30b2ad6e --- /dev/null +++ b/backend/src/middleware/paginate.js @@ -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(/^-/, ''); + + 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 + } + }); +}; \ No newline at end of file diff --git a/backend/src/routes/resume.js b/backend/src/routes/resume.js index e25f3db35..e60591c64 100644 --- a/backend/src/routes/resume.js +++ b/backend/src/routes/resume.js @@ -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