Implement Phase 1 admin dashboard backend

- Add database indexes for admin queries (role, isActive+role, createdAt, lastLoginAt)
- Update setUserActive to track disabledAt and disabledBy fields
- Add getPaginatedUsers function with search, filter, sort support
- Add updateUserRole function for promoting/demoting users
- Create admin routes with full user management API:
  - GET /api/admin/users - paginated list with search/filter/sort
  - GET /api/admin/users/:id - user details with recent activity
  - PATCH /api/admin/users/:id - enable/disable users
  - PATCH /api/admin/users/:id/role - promote/demote users
- Add validation for ObjectId format and self-modification prevention
- All 53 Phase 1 backend tests passing
This commit is contained in:
2026-01-22 12:01:52 -07:00
parent 39bd9b1e71
commit dbd8d81668
3 changed files with 363 additions and 12 deletions

207
routes/admin.js Normal file
View File

@ -0,0 +1,207 @@
const express = require('express');
const { ObjectId } = require('mongodb');
const { requireAuth, requireAdmin } = require('../middleware/auth');
const {
findById,
setUserActive,
getPaginatedUsers,
updateUserRole,
isValidObjectId
} = require('../models/user');
const { logActivity } = require('../services/activityLogger');
const router = express.Router({ strict: true });
// All admin routes require authentication and admin role
router.use(requireAuth, requireAdmin);
// Handle trailing slash on /users/ as invalid (empty ID)
router.get('/users/', (req, res) => {
return res.status(400).json({ error: 'Invalid user ID format' });
});
/**
* GET /api/admin/users
* Returns paginated list of users with optional filtering and sorting
*/
router.get('/users', async (req, res) => {
try {
const db = req.app.locals.db;
const { page, limit, search, status, sort, order } = req.query;
const result = await getPaginatedUsers(db, {
page,
limit,
search,
status,
sort,
order
});
res.json(result);
} catch (error) {
console.error('Error fetching users:', error);
res.status(500).json({ error: 'Failed to fetch users' });
}
});
/**
* GET /api/admin/users/:id
* Returns detailed user information including recent activity
*/
router.get('/users/:id', async (req, res) => {
try {
const db = req.app.locals.db;
const { id } = req.params;
// Validate ObjectId format
if (!isValidObjectId(id)) {
return res.status(400).json({ error: 'Invalid user ID format' });
}
const user = await findById(db, id);
if (!user) {
return res.status(404).json({ error: 'User not found' });
}
// Fetch recent activity for this user
const recentActivity = await db.collection('user_activity')
.find({ userId: user._id.toString() })
.sort({ timestamp: -1 })
.limit(20)
.toArray();
res.json({
user: {
...user,
recentActivity
}
});
} catch (error) {
console.error('Error fetching user details:', error);
res.status(500).json({ error: 'Failed to fetch user details' });
}
});
/**
* PATCH /api/admin/users/:id
* Enable or disable a user account
*/
router.patch('/users/:id', async (req, res) => {
try {
const db = req.app.locals.db;
const { id } = req.params;
const { isActive } = req.body;
// Validate ObjectId format
if (!isValidObjectId(id)) {
return res.status(400).json({ error: 'Invalid user ID format' });
}
// Check if isActive is provided
if (typeof isActive !== 'boolean') {
return res.status(400).json({ error: 'isActive field is required' });
}
// Check if admin is trying to disable themselves
if (req.user._id.toString() === id && !isActive) {
return res.status(400).json({ error: 'Cannot disable yourself' });
}
// Check if user exists
const existingUser = await findById(db, id);
if (!existingUser) {
return res.status(404).json({ error: 'User not found' });
}
// Update user status
const updatedUser = await setUserActive(db, id, isActive, req.user._id);
// Log admin action
await logActivity(db, {
userId: req.user._id.toString(),
action: isActive ? 'ADMIN_ENABLE_USER' : 'ADMIN_DISABLE_USER',
metadata: {
targetUserId: id,
targetUserEmail: existingUser.email
}
});
res.json({
user: updatedUser,
message: isActive ? 'User enabled successfully' : 'User disabled successfully'
});
} catch (error) {
console.error('Error updating user status:', error);
res.status(500).json({ error: 'Failed to update user status' });
}
});
/**
* PATCH /api/admin/users/:id/role
* Promote or demote a user (change role)
*/
router.patch('/users/:id/role', async (req, res) => {
try {
const db = req.app.locals.db;
const { id } = req.params;
const { role } = req.body;
// Validate ObjectId format
if (!isValidObjectId(id)) {
return res.status(400).json({ error: 'Invalid user ID format' });
}
// Validate role is provided
if (!role) {
return res.status(400).json({ error: 'Role is required' });
}
// Validate role value
if (!['user', 'admin'].includes(role)) {
return res.status(400).json({ error: 'Invalid role. Must be "user" or "admin"' });
}
// Check if admin is trying to demote themselves
if (req.user._id.toString() === id && role === 'user') {
return res.status(400).json({ error: 'Cannot demote yourself from admin' });
}
// Check if user exists
const existingUser = await findById(db, id);
if (!existingUser) {
return res.status(404).json({ error: 'User not found' });
}
// Update user role
const updatedUser = await updateUserRole(db, id, role);
// Log admin action
const action = role === 'admin' ? 'ADMIN_PROMOTE_USER' : 'ADMIN_DEMOTE_USER';
await logActivity(db, {
userId: req.user._id.toString(),
action,
metadata: {
targetUserId: id,
targetUserEmail: existingUser.email,
previousRole: existingUser.role,
newRole: role
}
});
const message = role === 'admin'
? 'User promoted to admin successfully'
: 'User demoted to user successfully';
res.json({
user: updatedUser,
message
});
} catch (error) {
console.error('Error updating user role:', error);
res.status(500).json({ error: 'Failed to update user role' });
}
});
module.exports = router;