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:
131
models/user.js
131
models/user.js
@ -73,15 +73,37 @@ async function findById(db, id) {
|
||||
* @param {Db} db - MongoDB database instance
|
||||
* @param {string|ObjectId} id - User's MongoDB _id
|
||||
* @param {boolean} isActive - New active status
|
||||
* @param {string|ObjectId|null} adminId - Admin performing the action (required when disabling)
|
||||
* @returns {Promise<Object>} Update result
|
||||
*/
|
||||
async function setUserActive(db, id, isActive) {
|
||||
async function setUserActive(db, id, isActive, adminId = null) {
|
||||
// Convert string to ObjectId if needed
|
||||
const objectId = typeof id === 'string' ? new ObjectId(id) : id;
|
||||
|
||||
let updateDoc;
|
||||
if (isActive) {
|
||||
// Enabling: clear disabledAt and disabledBy
|
||||
updateDoc = {
|
||||
$set: { isActive: true, disabledAt: null, disabledBy: null }
|
||||
};
|
||||
} else {
|
||||
// Disabling: set disabledAt and disabledBy
|
||||
// Convert string to ObjectId if needed, keep ObjectId as-is
|
||||
const adminObjectId = adminId && typeof adminId === 'string'
|
||||
? new ObjectId(adminId)
|
||||
: adminId;
|
||||
updateDoc = {
|
||||
$set: {
|
||||
isActive: false,
|
||||
disabledAt: new Date(),
|
||||
disabledBy: adminObjectId
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
const result = await db.collection(USER_COLLECTION).findOneAndUpdate(
|
||||
{ _id: objectId },
|
||||
{ $set: { isActive } },
|
||||
updateDoc,
|
||||
{ returnDocument: 'after' }
|
||||
);
|
||||
|
||||
@ -115,14 +137,117 @@ async function createIndexes(db) {
|
||||
{ isActive: 1, lastLoginAt: -1 }
|
||||
);
|
||||
|
||||
// Index on role for admin queries
|
||||
await collection.createIndex({ role: 1 });
|
||||
|
||||
// Compound index on isActive and role for filtered admin queries
|
||||
await collection.createIndex({ isActive: 1, role: 1 });
|
||||
|
||||
// Index on createdAt for recent registrations
|
||||
await collection.createIndex({ createdAt: -1 });
|
||||
|
||||
// Index on lastLoginAt for recently active users
|
||||
await collection.createIndex({ lastLoginAt: -1 });
|
||||
|
||||
console.log('User collection indexes created successfully');
|
||||
}
|
||||
|
||||
/**
|
||||
* Get paginated list of users with optional filtering and sorting
|
||||
*
|
||||
* @param {Db} db - MongoDB database instance
|
||||
* @param {Object} options - Query options
|
||||
* @param {number} options.page - Page number (1-indexed)
|
||||
* @param {number} options.limit - Items per page (max 100)
|
||||
* @param {string} options.search - Search term for name/email
|
||||
* @param {string} options.status - Filter by status: 'all', 'active', 'disabled'
|
||||
* @param {string} options.sort - Sort field: 'createdAt', 'lastLoginAt', 'loginCount'
|
||||
* @param {string} options.order - Sort order: 'asc', 'desc'
|
||||
* @returns {Promise<{users: Array, pagination: Object}>}
|
||||
*/
|
||||
async function getPaginatedUsers(db, options = {}) {
|
||||
const page = Math.max(1, parseInt(options.page) || 1);
|
||||
const limit = Math.min(100, Math.max(1, parseInt(options.limit) || 20));
|
||||
const skip = (page - 1) * limit;
|
||||
|
||||
// Build filter
|
||||
const filter = {};
|
||||
if (options.search) {
|
||||
const searchRegex = new RegExp(options.search, 'i');
|
||||
filter.$or = [{ name: searchRegex }, { email: searchRegex }];
|
||||
}
|
||||
if (options.status === 'active') {
|
||||
filter.isActive = true;
|
||||
} else if (options.status === 'disabled') {
|
||||
filter.isActive = false;
|
||||
}
|
||||
|
||||
// Build sort
|
||||
const sortField = ['createdAt', 'lastLoginAt', 'loginCount'].includes(options.sort)
|
||||
? options.sort
|
||||
: 'createdAt';
|
||||
const sortOrder = options.order === 'asc' ? 1 : -1;
|
||||
const sort = { [sortField]: sortOrder };
|
||||
|
||||
const collection = db.collection(USER_COLLECTION);
|
||||
|
||||
// Execute queries in parallel
|
||||
const [users, total] = await Promise.all([
|
||||
collection.find(filter).sort(sort).skip(skip).limit(limit).toArray(),
|
||||
collection.countDocuments(filter)
|
||||
]);
|
||||
|
||||
return {
|
||||
users,
|
||||
pagination: {
|
||||
page,
|
||||
limit,
|
||||
total,
|
||||
pages: Math.ceil(total / limit)
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Update a user's role
|
||||
*
|
||||
* @param {Db} db - MongoDB database instance
|
||||
* @param {string|ObjectId} id - User's MongoDB _id
|
||||
* @param {string} role - New role ('user' or 'admin')
|
||||
* @returns {Promise<Object|null>} Updated user or null
|
||||
*/
|
||||
async function updateUserRole(db, id, role) {
|
||||
const objectId = typeof id === 'string' ? new ObjectId(id) : id;
|
||||
|
||||
const result = await db.collection(USER_COLLECTION).findOneAndUpdate(
|
||||
{ _id: objectId },
|
||||
{ $set: { role } },
|
||||
{ returnDocument: 'after' }
|
||||
);
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if a string is a valid MongoDB ObjectId
|
||||
*
|
||||
* @param {string} id - String to validate
|
||||
* @returns {boolean} True if valid ObjectId format
|
||||
*/
|
||||
function isValidObjectId(id) {
|
||||
if (typeof id !== 'string') return false;
|
||||
if (!id || !id.trim()) return false;
|
||||
return /^[0-9a-fA-F]{24}$/.test(id);
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
USER_COLLECTION,
|
||||
findOrCreateUser,
|
||||
findByGoogleId,
|
||||
findById,
|
||||
setUserActive,
|
||||
createIndexes
|
||||
createIndexes,
|
||||
getPaginatedUsers,
|
||||
updateUserRole,
|
||||
isValidObjectId
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user