The admin panel is a separate Angular feature module — accessible only to users with the admin role — that provides system-wide oversight of all workspaces, users, tasks, and background jobs. It surfaces the operational metrics from Chapter 22 in a management interface: queue health, system stats, user management, and audit logs. The admin panel demonstrates role-based UI at the feature module level (not just individual components) and shows how to build operator-facing tooling on top of the production application infrastructure.
Admin Panel Features
| Section | Data Source | Actions |
|---|---|---|
| System Overview | GET /admin/stats | View — total users, workspaces, tasks, queue depth |
| User Management | GET /admin/users | Search, view profile, suspend/reactivate, change role |
| Workspace Management | GET /admin/workspaces | List all, view members, force-delete |
| Queue Monitor | GET /admin/queues | View waiting/active/completed/failed job counts, retry failed |
| Audit Log | GET /admin/audit-log | Search by user/workspace/action, paginated |
| Health Dashboard | GET /health + Prometheus metrics | Live system health, dependency status |
adminGuard that checks both authentication AND the admin role: if (!authStore.isAuthenticated()) → /auth/login, if (!authStore.isAdmin()) → /403. The guard applies at the module level in the routing config — every route within /admin/** is protected by the same guard without repeating it on individual routes. This is the correct pattern for feature-level access control: one guard at the module boundary rather than per-component checks.@bull-board/express) provides a pre-built queue management UI that can be mounted in your Express app at /admin/queues. It shows job counts, job details, and allows retrying failed jobs without any custom UI code. Mount it with admin authentication middleware: router.use('/admin/queues', verifyAccessToken, requireRole('admin'), bullBoardRouter). This is faster to ship than a custom queue UI and covers all the operational needs for queue management.Complete Admin Panel Implementation
// ── Admin routes — Express ────────────────────────────────────────────────
// apps/api/src/modules/admin/admin.routes.js
const { Router } = require('express');
const { verifyAccessToken } = require('../../middleware/auth.middleware');
const { requireRole } = require('../../middleware/rbac.middleware');
const { adminRateLimiter } = require('../../middleware/rate-limiter');
const adminController = require('./admin.controller');
const AuditLog = require('../audit/audit-log.model');
const router = Router();
// All admin routes require admin role
router.use(verifyAccessToken, requireRole('admin'), adminRateLimiter);
router.get('/stats', adminController.getSystemStats);
router.get('/users', adminController.getUsers);
router.patch('/users/:id/role', adminController.changeUserRole);
router.patch('/users/:id/status', adminController.toggleUserStatus);
router.get('/workspaces', adminController.getWorkspaces);
router.delete('/workspaces/:id', adminController.forceDeleteWorkspace);
router.get('/queues', adminController.getQueueStats);
router.post('/queues/:name/retry-failed', adminController.retryFailedJobs);
router.get('/audit-log', adminController.getAuditLog);
module.exports = router;
// ── Admin controller (stats + queue monitoring) ───────────────────────────
const asyncHandler = require('express-async-handler');
const mongoose = require('mongoose');
const User = require('../users/user.model');
const Task = require('../tasks/task.model');
const Workspace = require('../workspaces/workspace.model');
const { emailQueue, reportQueue, notifyQueue } = require('../../queues');
const AuditLog = require('../audit/audit-log.model');
exports.getSystemStats = asyncHandler(async (req, res) => {
const [userCount, taskCount, workspaceCount, activeTaskCount, queueStats] = await Promise.all([
User.countDocuments({ isActive: true }),
Task.countDocuments({ deletedAt: { $exists: false } }),
Workspace.countDocuments(),
Task.countDocuments({ status: { $in: ['todo','in-progress','in-review'] }, deletedAt: { $exists: false } }),
getQueueStats(),
]);
res.json({
success: true,
data: {
users: { total: userCount },
tasks: { total: taskCount, active: activeTaskCount },
workspaces: { total: workspaceCount },
queues: queueStats,
uptime: process.uptime(),
memory: process.memoryUsage(),
nodeVersion: process.version,
},
});
});
async function getQueueStats() {
const [email, report, notify] = await Promise.all([
emailQueue.getJobCounts(),
reportQueue.getJobCounts(),
notifyQueue.getJobCounts(),
]);
return {
email: { ...email, name: 'email' },
report: { ...report, name: 'report' },
notify: { ...notify, name: 'notify' },
};
}
exports.retryFailedJobs = asyncHandler(async (req, res) => {
const queueMap = { email: emailQueue, report: reportQueue, notify: notifyQueue };
const queue = queueMap[req.params.name];
if (!queue) return res.status(404).json({ message: 'Queue not found' });
const failed = await queue.getFailed();
await Promise.all(failed.map(job => job.retry()));
// Log admin action
await AuditLog.create({
actor: req.user.sub,
action: 'ADMIN_RETRY_FAILED_JOBS',
resource: { type: 'queue', id: req.params.name },
metadata: { count: failed.length },
});
res.json({ success: true, data: { retried: failed.length } });
});
exports.toggleUserStatus = asyncHandler(async (req, res) => {
const user = await User.findByIdAndUpdate(
req.params.id,
[{ $set: { isActive: { $not: '$isActive' } } }], // toggle
{ new: true }
);
if (!user) return res.status(404).json({ message: 'User not found' });
await AuditLog.create({
actor: req.user.sub,
action: user.isActive ? 'ADMIN_USER_ACTIVATED' : 'ADMIN_USER_SUSPENDED',
resource: { type: 'user', id: user._id },
metadata: { targetEmail: user.email },
});
res.json({ success: true, data: { id: user._id, isActive: user.isActive } });
});
// ── Angular admin guard ───────────────────────────────────────────────────
// core/guards/admin.guard.ts
import { inject } from '@angular/core';
import { Router } from '@angular/router';
import { AuthStore }from '../stores/auth.store';
export const adminGuard = () => {
const authStore = inject(AuthStore);
const router = inject(Router);
if (!authStore.isAuthenticated()) {
return router.createUrlTree(['/auth/login']);
}
if (!authStore.isAdmin()) {
return router.createUrlTree(['/403']);
}
return true;
};
// Admin routes
// { path: 'admin', canActivate: [adminGuard], loadChildren: () => import('./features/admin/admin.routes') }
How It Works
Step 1 — Three-Layer Admin Route Protection
Admin routes apply verifyAccessToken (authentication), requireRole('admin') (authorisation), and adminRateLimiter (abuse prevention) as a middleware chain. This defence in depth means: an unauthenticated request gets 401, an authenticated non-admin gets 403, and an authenticated admin who makes too many requests gets 429. No admin operation can bypass any of these checks because they are applied to the entire router rather than individual endpoints.
Step 2 — Toggle Pattern with Aggregation Pipeline Update
The toggle expression [{ $set: { isActive: { $not: '$isActive' } } }] is a MongoDB aggregation pipeline update — it reads the current value and inverts it atomically. Without this, the code would need to: read the current value, compute the inverse, write the new value. This read-modify-write pattern has a race condition window. The aggregation pipeline update is atomic — no race condition is possible.
Step 3 — Audit Log Records Every Admin Action
Every state-changing admin action writes an audit log entry with the actor (admin’s user ID), the action (a string constant), the affected resource (type + ID), and relevant metadata. This creates a complete audit trail: who did what to which resource and when. The audit log is queryable via the admin panel — if a user is suspended, an admin can search for ADMIN_USER_SUSPENDED actions to see which admin did it and when.
Step 4 — Bull Board Provides Zero-Code Queue Management UI
Installing @bull-board/express and mounting it at /admin/queues-ui provides a full-featured queue management interface: job counts, job details, job payloads, retry failed jobs, clear completed jobs, and real-time job progress. This is production-ready operational tooling that would take days to build from scratch, provided free as an open-source library. The only requirement is protecting it with admin authentication middleware.
Step 5 — Feature-Level Guard Eliminates Per-Route Repetition
Applying canActivate: [adminGuard] to the /admin route entry (the one that lazy-loads the admin feature module) protects every route within that feature. New admin routes added to the admin module are automatically protected — the developer cannot forget to add the guard. This is superior to adding the guard to every individual admin route, where missing it on one route creates a security hole.
Quick Reference
| Task | Code |
|---|---|
| Admin route group | router.use(verifyAccessToken, requireRole('admin'), adminRateLimiter) |
| Atomic toggle | { $set: { isActive: { $not: '$isActive' } } } (aggregation pipeline update) |
| Queue stats | queue.getJobCounts() → { waiting, active, completed, failed } |
| Retry failed jobs | const failed = await queue.getFailed(); failed.map(j => j.retry()) |
| Audit log entry | AuditLog.create({ actor, action, resource: { type, id }, metadata }) |
| Angular admin guard | canActivate: [adminGuard] on the admin module route |
| Bull Board UI | npm install @bull-board/express + mount with auth middleware |