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
115 changes: 115 additions & 0 deletions src/__tests__/admin.users.test.js
Original file line number Diff line number Diff line change
@@ -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');
});
});
86 changes: 82 additions & 4 deletions src/controllers/admin.users.controller.js
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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();
Expand Down Expand Up @@ -164,6 +241,7 @@ const updateUserRole = async (req, res, next) => {

module.exports = {
deleteUser,
getUserById,
restoreUser,
listUsers,
updateUserStatus,
Expand Down
4 changes: 2 additions & 2 deletions src/middlewares/isAdmin.js
Original file line number Diff line number Diff line change
Expand Up @@ -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();
Expand Down
1 change: 1 addition & 0 deletions src/models/User.model.js
Original file line number Diff line number Diff line change
Expand Up @@ -84,6 +84,7 @@ const userSchema = new mongoose.Schema(
avatar: {
type: String,
trim: true,
},
// Soft delete field
deletedAt: {
type: Date,
Expand Down
7 changes: 7 additions & 0 deletions src/routes/admin.routes.js
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ const isAdmin = require('../middlewares/isAdmin');
const validate = require('../middlewares/validate');
const {
deleteUser,
getUserById,
restoreUser,
listUsers,
updateUserStatus,
Expand All @@ -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);

Expand Down
Loading