From e0fd1e2632c6de1fb8c004a671ac49c5a95f72fc Mon Sep 17 00:00:00 2001 From: Musa Khalid <112591148+Mkalbani@users.noreply.github.com> Date: Fri, 19 Jun 2026 13:30:03 +0000 Subject: [PATCH 1/2] feat: add admin user detail and role endpoints --- src/__tests__/admin.users.test.js | 115 ++++++++++++++++++++++ src/controllers/admin.users.controller.js | 93 ++++++++++++++++- src/middlewares/isAdmin.js | 4 +- src/routes/admin.routes.js | 8 ++ 4 files changed, 214 insertions(+), 6 deletions(-) create mode 100644 src/__tests__/admin.users.test.js diff --git a/src/__tests__/admin.users.test.js b/src/__tests__/admin.users.test.js new file mode 100644 index 0000000..9425dbb --- /dev/null +++ b/src/__tests__/admin.users.test.js @@ -0,0 +1,115 @@ +const jwt = require('jsonwebtoken'); +const request = require('supertest'); + +jest.mock('../models/User.model', () => ({ + findById: jest.fn(), + findByIdAndUpdate: jest.fn(), +})); + +const User = require('../models/User.model'); +const app = require('../app'); + +describe('Admin user management routes', () => { + const adminId = '507f1f77bcf86cd799439099'; + const targetUserId = '507f1f77bcf86cd799439011'; + let token; + + beforeAll(() => { + token = jwt.sign( + { sub: adminId, type: 'access' }, + process.env.JWT_SECRET || 'test-secret', + { expiresIn: '1h' } + ); + }); + + beforeEach(() => { + jest.clearAllMocks(); + User.findById.mockImplementation((id) => { + if (id === adminId) { + return Promise.resolve({ + _id: adminId, + role: 'admin', + }); + } + if (id === targetUserId) { + return Promise.resolve({ + _id: targetUserId, + fullName: 'Jane Doe', + email: 'jane@example.com', + role: 'user', + isVerified: true, + kycStatus: 'approved', + kycSubmissionDate: new Date('2026-06-18T10:00:00.000Z'), + kycReviewNotes: 'Documents verified', + walletAddress: 'GABC123', + avatar: 'uploads/avatar.png', + deletedAt: null, + }); + } + return Promise.resolve(null); + }); + }); + + it('returns full user details for a valid admin request', async () => { + const response = await request(app) + .get(`/api/admin/users/${targetUserId}`) + .set('Authorization', `Bearer ${token}`) + .expect(200); + + expect(response.body.success).toBe(true); + expect(response.body.message).toBe('User retrieved successfully'); + expect(response.body.data).toMatchObject({ + id: targetUserId, + email: 'jane@example.com', + role: 'user', + kycStatus: 'approved', + }); + }); + + it('returns 404 when the requested user does not exist', async () => { + const response = await request(app) + .get('/api/admin/users/507f1f77bcf86cd799439999') + .set('Authorization', `Bearer ${token}`) + .expect(404); + + expect(response.body.success).toBe(false); + expect(response.body.message).toBe('User not found'); + }); + + it('updates a user role for another admin request', async () => { + User.findByIdAndUpdate.mockResolvedValue({ + _id: targetUserId, + fullName: 'Jane Doe', + email: 'jane@example.com', + role: 'admin', + isVerified: true, + kycStatus: 'approved', + kycSubmissionDate: new Date('2026-06-18T10:00:00.000Z'), + kycReviewNotes: 'Documents verified', + walletAddress: 'GABC123', + avatar: 'uploads/avatar.png', + deletedAt: null, + }); + + const response = await request(app) + .patch(`/api/admin/users/${targetUserId}/role`) + .set('Authorization', `Bearer ${token}`) + .send({ role: 'admin' }) + .expect(200); + + expect(response.body.success).toBe(true); + expect(response.body.message).toBe('User role updated successfully'); + expect(response.body.data.role).toBe('admin'); + }); + + it('prevents an admin from downgrading their own role', async () => { + const response = await request(app) + .patch(`/api/admin/users/${adminId}/role`) + .set('Authorization', `Bearer ${token}`) + .send({ role: 'user' }) + .expect(403); + + expect(response.body.success).toBe(false); + expect(response.body.message).toBe('You cannot change your own role'); + }); +}); diff --git a/src/controllers/admin.users.controller.js b/src/controllers/admin.users.controller.js index f419e89..0c2a7d3 100644 --- a/src/controllers/admin.users.controller.js +++ b/src/controllers/admin.users.controller.js @@ -1,6 +1,89 @@ const User = require('../models/User.model'); const { sendSuccess, sendError } = require('../utils/response'); +/** + * Get a specific user profile (admin only) + * @route GET /api/admin/users/:id + * @access Admin only + */ +const getUserById = async (req, res, next) => { + try { + const { id } = req.params; + + const user = await User.findById(id); + + if (!user) { + return sendError(res, 'User not found', 404); + } + + return sendSuccess(res, { + id: user._id.toString(), + fullName: user.fullName, + email: user.email, + role: user.role, + isVerified: user.isVerified, + kycStatus: user.kycStatus, + kycSubmissionDate: user.kycSubmissionDate, + kycReviewNotes: user.kycReviewNotes, + walletAddress: user.walletAddress, + avatar: user.avatar, + createdAt: user.createdAt, + updatedAt: user.updatedAt, + deletedAt: user.deletedAt, + }, 200, 'User retrieved successfully'); + } catch (error) { + next(error); + } +}; + +/** + * Update a user's role (admin only) + * @route PATCH /api/admin/users/:id/role + * @access Admin only + */ +const updateUserRole = async (req, res, next) => { + try { + const { id } = req.params; + const { role } = req.body; + + if (!['user', 'admin'].includes(role)) { + return sendError(res, 'Role must be either user or admin', 400); + } + + if (id === req.userId) { + return sendError(res, 'You cannot change your own role', 403); + } + + const updatedUser = await User.findByIdAndUpdate( + id, + { $set: { role } }, + { new: true, runValidators: true } + ); + + if (!updatedUser) { + return sendError(res, 'User not found', 404); + } + + return sendSuccess(res, { + id: updatedUser._id.toString(), + fullName: updatedUser.fullName, + email: updatedUser.email, + role: updatedUser.role, + isVerified: updatedUser.isVerified, + kycStatus: updatedUser.kycStatus, + kycSubmissionDate: updatedUser.kycSubmissionDate, + kycReviewNotes: updatedUser.kycReviewNotes, + walletAddress: updatedUser.walletAddress, + avatar: updatedUser.avatar, + createdAt: updatedUser.createdAt, + updatedAt: updatedUser.updatedAt, + deletedAt: updatedUser.deletedAt, + }, 200, 'User role updated successfully'); + } catch (error) { + next(error); + } +}; + /** * Soft delete a user by ID (admin only) * @route DELETE /api/admin/users/:id @@ -12,18 +95,18 @@ const deleteUser = async (req, res, next) => { // Prevent admin from deleting their own account if (id === req.userId) { - return sendError(res, 403, 'You cannot delete your own account'); + return sendError(res, 'You cannot delete your own account', 403); } // Find the user const user = await User.findById(id); if (!user) { - return sendError(res, 404, 'User not found'); + return sendError(res, 'User not found', 404); } // Check if already deleted if (user.deletedAt) { - return sendError(res, 400, 'User already deleted'); + return sendError(res, 'User already deleted', 400); } // Soft delete the user @@ -51,7 +134,7 @@ const restoreUser = async (req, res, next) => { const user = await User.findOne({ _id: id, deletedAt: { $ne: null } }); if (!user) { - return sendError(res, 404, 'User not found or not deleted'); + return sendError(res, 'User not found or not deleted', 404); } await user.restore(); @@ -88,6 +171,8 @@ const listUsers = async (req, res, next) => { module.exports = { deleteUser, + getUserById, restoreUser, listUsers, + updateUserRole, }; diff --git a/src/middlewares/isAdmin.js b/src/middlewares/isAdmin.js index 44b2b91..963bf54 100644 --- a/src/middlewares/isAdmin.js +++ b/src/middlewares/isAdmin.js @@ -6,11 +6,11 @@ const { sendError } = require('../utils/response'); */ const isAdmin = (req, res, next) => { if (!req.user) { - return sendError(res, 401, 'Unauthorized'); + return sendError(res, 'Unauthorized', 401); } if (req.user.role !== 'admin') { - return sendError(res, 403, 'Admin access required'); + return sendError(res, 'Admin access required', 403); } next(); diff --git a/src/routes/admin.routes.js b/src/routes/admin.routes.js index 513041e..230954a 100644 --- a/src/routes/admin.routes.js +++ b/src/routes/admin.routes.js @@ -3,8 +3,10 @@ const authenticate = require('../middlewares/auth'); const isAdmin = require('../middlewares/isAdmin'); const { deleteUser, + getUserById, restoreUser, listUsers, + updateUserRole, } = require('../controllers/admin.users.controller'); const router = express.Router(); @@ -16,6 +18,12 @@ router.use(isAdmin); // GET /api/admin/users - List all users (optionally include deleted) router.get('/users', listUsers); +// GET /api/admin/users/:id - Get a specific user profile +router.get('/users/:id', getUserById); + +// PATCH /api/admin/users/:id/role - Update a user's role +router.patch('/users/:id/role', updateUserRole); + // DELETE /api/admin/users/:id - Soft delete a user router.delete('/users/:id', deleteUser); From 80f25c5a82c70ee202edb0ae9588def0ede5e304 Mon Sep 17 00:00:00 2001 From: Musa Khalid <112591148+Mkalbani@users.noreply.github.com> Date: Fri, 19 Jun 2026 13:30:03 +0000 Subject: [PATCH 2/2] feat: add admin user detail and role endpoints --- src/models/User.model.js | 1 + 1 file changed, 1 insertion(+) diff --git a/src/models/User.model.js b/src/models/User.model.js index ed53406..c3dbb07 100644 --- a/src/models/User.model.js +++ b/src/models/User.model.js @@ -79,6 +79,7 @@ const userSchema = new mongoose.Schema( avatar: { type: String, trim: true, + }, // Soft delete field deletedAt: { type: Date,