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 1acc80e..3724947 100644 --- a/src/controllers/admin.users.controller.js +++ b/src/controllers/admin.users.controller.js @@ -2,6 +2,83 @@ 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'); * Suspend or activate a user account (admin only) * @route PATCH /api/admin/users/:id/status * @access Admin only @@ -46,18 +123,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 @@ -85,7 +162,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(); @@ -164,6 +241,7 @@ const updateUserRole = async (req, res, next) => { module.exports = { deleteUser, + getUserById, restoreUser, listUsers, updateUserStatus, 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/models/User.model.js b/src/models/User.model.js index 7cc90fe..1640483 100644 --- a/src/models/User.model.js +++ b/src/models/User.model.js @@ -84,6 +84,7 @@ const userSchema = new mongoose.Schema( avatar: { type: String, trim: true, + }, // Soft delete field deletedAt: { type: Date, diff --git a/src/routes/admin.routes.js b/src/routes/admin.routes.js index 9c22b4e..ad60fe5 100644 --- a/src/routes/admin.routes.js +++ b/src/routes/admin.routes.js @@ -4,6 +4,7 @@ const isAdmin = require('../middlewares/isAdmin'); const validate = require('../middlewares/validate'); const { deleteUser, + getUserById, restoreUser, listUsers, updateUserStatus, @@ -21,6 +22,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);