From 3a52b8c494f9b57a5f00daf892ad0324a5d49606 Mon Sep 17 00:00:00 2001 From: ummarig Date: Sun, 21 Jun 2026 14:51:54 +0000 Subject: [PATCH 1/4] implemented the upload project cover image --- src/controllers/project.controller.js | 43 ++++++++++++++++++++++++++- src/models/Project.model.js | 1 + 2 files changed, 43 insertions(+), 1 deletion(-) diff --git a/src/controllers/project.controller.js b/src/controllers/project.controller.js index cce47b8..8881523 100644 --- a/src/controllers/project.controller.js +++ b/src/controllers/project.controller.js @@ -1,6 +1,47 @@ const Project = require('../models/Project.model'); const { sendSuccess } = require('../utils/response'); +/** + * Get a single project by id + * GET /api/projects/:id + */ +const getProjectById = async (req, res, next) => { + try { + const { id } = req.params; + + const project = await Project.findById(id) + .populate('owner', 'fullName') + .exec(); + + if (!project) { + const error = new Error('Project not found'); + error.statusCode = 404; + error.isOperational = true; + return next(error); + } + + const ownerId = project.owner && project.owner._id ? project.owner._id.toString() : project.owner?.toString(); + const isOwner = req.userId && ownerId === req.userId; + const isAdmin = req.user?.role === 'admin'; + + if (project.isActive === false && !isOwner && !isAdmin) { + const error = new Error('Project not found'); + error.statusCode = 404; + error.isOperational = true; + return next(error); + } + + const responseProject = project.toObject(); + if (responseProject.owner && responseProject.owner.fullName) { + responseProject.owner = { fullName: responseProject.owner.fullName }; + } + + return sendSuccess(res, { project: responseProject }, 200, 'Project retrieved successfully'); + } catch (error) { + return next(error); + } +}; + /** * Upload supporting documents to a project * POST /api/projects/:id/documents @@ -69,4 +110,4 @@ const uploadDocuments = async (req, res, next) => { } }; -module.exports = { uploadDocuments }; \ No newline at end of file +module.exports = { getProjectById, uploadDocuments }; \ No newline at end of file diff --git a/src/models/Project.model.js b/src/models/Project.model.js index 136c53f..6f92a0d 100644 --- a/src/models/Project.model.js +++ b/src/models/Project.model.js @@ -17,6 +17,7 @@ const projectSchema = new mongoose.Schema( title: { type: String, required: true, trim: true }, description: { type: String, trim: true }, owner: { type: mongoose.Schema.Types.ObjectId, ref: 'User', required: true }, + isActive: { type: Boolean, default: true }, documents: { type: [documentSchema], default: [] }, // ... add your other project fields here }, From 882a2f651f81eceff6ecc4d073c48da49c9bce02 Mon Sep 17 00:00:00 2001 From: ummarig Date: Sun, 21 Jun 2026 14:53:13 +0000 Subject: [PATCH 2/4] implemented the upload project cover image --- src/__tests__/project.routes.test.js | 132 +++++++++++++++++++++++++++ src/middlewares/auth.js | 31 +++++++ src/routes/project.routes.js | 11 ++- 3 files changed, 172 insertions(+), 2 deletions(-) create mode 100644 src/__tests__/project.routes.test.js diff --git a/src/__tests__/project.routes.test.js b/src/__tests__/project.routes.test.js new file mode 100644 index 0000000..14fc2c2 --- /dev/null +++ b/src/__tests__/project.routes.test.js @@ -0,0 +1,132 @@ +const jwt = require('jsonwebtoken'); +const request = require('supertest'); + +jest.mock('../models/Project.model', () => ({ + findById: jest.fn(), +})); + +const Project = require('../models/Project.model'); +const app = require('../app'); + +describe('GET /api/projects/:id', () => { + const projectId = '507f1f77bcf86cd799439066'; + const ownerId = '507f1f77bcf86cd799439055'; + const adminId = '507f1f77bcf86cd799439099'; + + const projectDoc = { + _id: projectId, + title: 'Campaign Test', + description: 'A valid active campaign', + owner: { _id: ownerId, fullName: 'Owner Name' }, + isActive: true, + documents: [], + toObject: function () { + return { + _id: this._id, + title: this.title, + description: this.description, + owner: this.owner, + isActive: this.isActive, + documents: this.documents, + }; + }, + }; + + beforeEach(() => { + jest.clearAllMocks(); + }); + + it('returns full project details for a valid active project id without auth', async () => { + Project.findById.mockReturnValueOnce({ + populate: jest.fn().mockReturnThis(), + exec: jest.fn().mockResolvedValueOnce(projectDoc), + }); + + const response = await request(app).get(`/api/projects/${projectId}`).expect(200); + + expect(response.body.success).toBe(true); + expect(response.body.message).toBe('Project retrieved successfully'); + expect(response.body.data.project).toMatchObject({ + _id: projectId, + title: 'Campaign Test', + description: 'A valid active campaign', + owner: { fullName: 'Owner Name' }, + isActive: true, + }); + }); + + it('returns 404 for a non-existent project id', async () => { + Project.findById.mockReturnValueOnce({ + populate: jest.fn().mockReturnThis(), + exec: jest.fn().mockResolvedValueOnce(null), + }); + + const response = await request(app).get(`/api/projects/${projectId}`).expect(404); + + expect(response.body.success).toBe(false); + expect(response.body.message).toBe('Project not found'); + }); + + it('returns 404 for an inactive project if requester is not owner/admin', async () => { + Project.findById.mockReturnValueOnce({ + populate: jest.fn().mockReturnThis(), + exec: jest.fn().mockResolvedValueOnce({ + ...projectDoc, + isActive: false, + }), + }); + + const response = await request(app).get(`/api/projects/${projectId}`).expect(404); + + expect(response.body.success).toBe(false); + expect(response.body.message).toBe('Project not found'); + }); + + it('returns inactive project details to owner', async () => { + const token = jwt.sign( + { sub: ownerId, type: 'access' }, + process.env.JWT_SECRET || 'test-secret', + { expiresIn: '1h' } + ); + + Project.findById.mockReturnValueOnce({ + populate: jest.fn().mockReturnThis(), + exec: jest.fn().mockResolvedValueOnce({ + ...projectDoc, + isActive: false, + }), + }); + + const response = await request(app) + .get(`/api/projects/${projectId}`) + .set('Authorization', `Bearer ${token}`) + .expect(200); + + expect(response.body.success).toBe(true); + expect(response.body.data.project.owner).toEqual({ fullName: 'Owner Name' }); + }); + + it('returns inactive project details to admin', async () => { + const token = jwt.sign( + { sub: adminId, type: 'access' }, + process.env.JWT_SECRET || 'test-secret', + { expiresIn: '1h' } + ); + + Project.findById.mockReturnValueOnce({ + populate: jest.fn().mockReturnThis(), + exec: jest.fn().mockResolvedValueOnce({ + ...projectDoc, + isActive: false, + }), + }); + + const response = await request(app) + .get(`/api/projects/${projectId}`) + .set('Authorization', `Bearer ${token}`) + .expect(200); + + expect(response.body.success).toBe(true); + expect(response.body.data.project.owner).toEqual({ fullName: 'Owner Name' }); + }); +}); diff --git a/src/middlewares/auth.js b/src/middlewares/auth.js index aedf172..5a1d8a3 100644 --- a/src/middlewares/auth.js +++ b/src/middlewares/auth.js @@ -56,4 +56,35 @@ const authenticate = async (req, res, next) => { } }; +const optionalAuthenticate = async (req, res, next) => { + try { + const authHeader = req.headers.authorization; + if (!authHeader || !authHeader.startsWith('Bearer ')) { + return next(); + } + + const token = authHeader.substring(7); + if (!token) { + return next(); + } + + const decoded = jwt.verify(token, process.env.JWT_SECRET); + if (decoded.type !== 'access') { + return next(); + } + + const user = await User.findById(decoded.sub); + if (!user) { + return next(); + } + + req.user = user; + req.userId = user._id.toString(); + next(); + } catch (_error) { + next(); + } +}; + module.exports = authenticate; +module.exports.optionalAuthenticate = optionalAuthenticate; diff --git a/src/routes/project.routes.js b/src/routes/project.routes.js index 2a5ae44..4744ff0 100644 --- a/src/routes/project.routes.js +++ b/src/routes/project.routes.js @@ -1,8 +1,15 @@ const express = require('express'); const router = express.Router(); -const { authenticate } = require('../middlewares/authenticate'); // your existing auth middleware +const authenticate = require('../middlewares/auth'); +const { optionalAuthenticate } = require('../middlewares/auth'); const { handleUpload } = require('../middlewares/upload.middleware'); -const { uploadDocuments } = require('../controllers/project.controller'); +const { getProjectById, uploadDocuments } = require('../controllers/project.controller'); + +/** + * GET /api/projects/:id + * Retrieve a single project by id + */ +router.get('/:id', optionalAuthenticate, getProjectById); /** * POST /api/projects/:id/documents From 8607748a0ff9470581c1f67e1596c1f57c6dae55 Mon Sep 17 00:00:00 2001 From: ummarig Date: Sun, 21 Jun 2026 14:53:58 +0000 Subject: [PATCH 3/4] implemented the upload project cover image --- src/models/User.model.js | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/src/models/User.model.js b/src/models/User.model.js index 1640483..781bf22 100644 --- a/src/models/User.model.js +++ b/src/models/User.model.js @@ -90,10 +90,8 @@ const userSchema = new mongoose.Schema( type: Date, default: null, }, - timestamps: true, - } -} - + }, + { timestamps: true } ); // Middleware to exclude soft-deleted users from all queries From 18b97ffdbb101684461361f574b94ef41ff97f88 Mon Sep 17 00:00:00 2001 From: ummarig Date: Sun, 21 Jun 2026 14:56:30 +0000 Subject: [PATCH 4/4] implemented the upload project cover image --- src/__tests__/project.routes.test.js | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/src/__tests__/project.routes.test.js b/src/__tests__/project.routes.test.js index 14fc2c2..48a3f2c 100644 --- a/src/__tests__/project.routes.test.js +++ b/src/__tests__/project.routes.test.js @@ -5,7 +5,12 @@ jest.mock('../models/Project.model', () => ({ findById: jest.fn(), })); +jest.mock('../models/User.model', () => ({ + findById: jest.fn(), +})); + const Project = require('../models/Project.model'); +const User = require('../models/User.model'); const app = require('../app'); describe('GET /api/projects/:id', () => { @@ -34,6 +39,15 @@ describe('GET /api/projects/:id', () => { beforeEach(() => { jest.clearAllMocks(); + User.findById.mockImplementation((id) => { + if (id === ownerId) { + return Promise.resolve({ _id: ownerId, role: 'user' }); + } + if (id === adminId) { + return Promise.resolve({ _id: adminId, role: 'admin' }); + } + return Promise.resolve(null); + }); }); it('returns full project details for a valid active project id without auth', async () => {