From 62341b27d59888c6ede6132a0645366c2b351fe6 Mon Sep 17 00:00:00 2001 From: gabito1451 Date: Thu, 5 Mar 2026 10:00:03 -0800 Subject: [PATCH] Implement the role-based authorization middleware and set up the infrastructure for admin-exclusive routes --- package-lock.json | 5 --- src/__tests__/authorize.test.js | 61 +++++++++++++++++++++++++++++++++ src/app.js | 4 +++ src/middlewares/authorize.js | 29 ++++++++++++++++ src/routes/admin.routes.js | 41 ++++++++++++++++++++++ 5 files changed, 135 insertions(+), 5 deletions(-) create mode 100644 src/__tests__/authorize.test.js create mode 100644 src/middlewares/authorize.js create mode 100644 src/routes/admin.routes.js diff --git a/package-lock.json b/package-lock.json index 14cd9bc..4da6308 100644 --- a/package-lock.json +++ b/package-lock.json @@ -64,7 +64,6 @@ "integrity": "sha512-CGOfOJqWjg2qW/Mb6zNsDm+u5vFQ8DxXfbM09z69p5Z6+mE1ikP2jUXw+j42Pf1XTYED2Rni5f95npYeuwMDQA==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "@babel/code-frame": "^7.29.0", "@babel/generator": "^7.29.0", @@ -1973,7 +1972,6 @@ "integrity": "sha512-UVJyE9MttOsBQIDKw1skb9nAwQuR5wuGD3+82K6JgJlm/Y+KI92oNsMNGZCYdDsVtRHSak0pcV5Dno5+4jh9sw==", "dev": true, "license": "MIT", - "peer": true, "bin": { "acorn": "bin/acorn" }, @@ -2314,7 +2312,6 @@ } ], "license": "MIT", - "peer": true, "dependencies": { "baseline-browser-mapping": "^2.9.0", "caniuse-lite": "^1.0.30001759", @@ -3034,7 +3031,6 @@ "integrity": "sha512-VmQ+sifHUbI/IcSopBCF/HO3YiHQx/AVd3UVyYL6weuwW+HvON9VYn5l6Zl1WZzPWXPNZrSQpxwkkZ/VuvJZzg==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "@eslint-community/eslint-utils": "^4.8.0", "@eslint-community/regexpp": "^4.12.1", @@ -3438,7 +3434,6 @@ "resolved": "https://registry.npmjs.org/express/-/express-5.2.1.tgz", "integrity": "sha512-hIS4idWWai69NezIdRt2xFVofaF4j+6INOpJlVOLDO8zXGpUVEVzIYk12UUi2JzjEzWL3IOAxcTubgz9Po0yXw==", "license": "MIT", - "peer": true, "dependencies": { "accepts": "^2.0.0", "body-parser": "^2.2.1", diff --git a/src/__tests__/authorize.test.js b/src/__tests__/authorize.test.js new file mode 100644 index 0000000..4c18120 --- /dev/null +++ b/src/__tests__/authorize.test.js @@ -0,0 +1,61 @@ +const authorize = require('../middlewares/authorize'); + +describe('Authorize Middleware', () => { + let req; + let res; + let next; + + beforeEach(() => { + req = { + user: null, + }; + res = { + status: jest.fn().mockReturnThis(), + json: jest.fn().mockReturnThis(), + }; + next = jest.fn(); + }); + + it('should call next if user has an allowed role', () => { + req.user = { role: 'admin' }; + const middleware = authorize('admin', 'superadmin'); + + middleware(req, res, next); + + expect(next).toHaveBeenCalledWith(); + expect(next).not.toHaveBeenCalledWith(expect.any(Error)); + }); + + it('should return 401 if user is not authenticated', () => { + req.user = null; + const middleware = authorize('admin'); + + middleware(req, res, next); + + expect(next).toHaveBeenCalledWith(expect.objectContaining({ + statusCode: 401, + message: 'Authentication required' + })); + }); + + it('should return 403 if user role is not allowed', () => { + req.user = { role: 'user' }; + const middleware = authorize('admin'); + + middleware(req, res, next); + + expect(next).toHaveBeenCalledWith(expect.objectContaining({ + statusCode: 403, + message: 'Access forbidden: insufficient permissions' + })); + }); + + it('should work with multiple allowed roles', () => { + req.user = { role: 'editor' }; + const middleware = authorize('admin', 'editor'); + + middleware(req, res, next); + + expect(next).toHaveBeenCalledWith(); + }); +}); diff --git a/src/app.js b/src/app.js index 4dabaa6..f6a3a15 100644 --- a/src/app.js +++ b/src/app.js @@ -7,6 +7,7 @@ const { getLoggerStream } = require('./utils/logger'); const { globalLimiter, authLimiter } = require('./middlewares/rateLimiter'); const authRoutes = require('./routes/auth.routes'); const protectedRoutes = require('./routes/protected.routes'); +const adminRoutes = require('./routes/admin.routes'); const app = express(); @@ -40,6 +41,9 @@ app.use('/api/auth', authLimiter, authRoutes); // Protected routes app.use('/api/protected', protectedRoutes); +// Admin routes +app.use('/api/admin', adminRoutes); + // Global error handling middleware - must be registered last const errorHandler = require('./middlewares/errorHandler'); app.use(errorHandler); diff --git a/src/middlewares/authorize.js b/src/middlewares/authorize.js new file mode 100644 index 0000000..4648037 --- /dev/null +++ b/src/middlewares/authorize.js @@ -0,0 +1,29 @@ +/** + * Middleware to restrict access by user role + * @param {...string} allowedRoles - Roles allowed to access the route + * @returns {Function} Middleware function + */ +const authorize = (...allowedRoles) => { + return (req, res, next) => { + // Check if user is authenticated (req.user must be populated by auth middleware) + if (!req.user) { + const error = new Error('Authentication required'); + error.statusCode = 401; + error.isOperational = true; + return next(error); + } + + // Check if user's role is in the list of allowed roles + if (!allowedRoles.includes(req.user.role)) { + const error = new Error('Access forbidden: insufficient permissions'); + error.statusCode = 403; + error.isOperational = true; + return next(error); + } + + // User is authorized + next(); + }; +}; + +module.exports = authorize; diff --git a/src/routes/admin.routes.js b/src/routes/admin.routes.js new file mode 100644 index 0000000..53b9ca3 --- /dev/null +++ b/src/routes/admin.routes.js @@ -0,0 +1,41 @@ +const express = require('express'); +const authenticate = require('../middlewares/auth'); +const authorize = require('../middlewares/authorize'); +const { sendSuccess } = require('../utils/response'); + +const router = express.Router(); + +/** + * Admin Routes + * All routes in this router require authentication and 'admin' role + */ + +// Global middleware for this router +router.use(authenticate); +router.use(authorize('admin')); + +// GET /api/admin/dashboard - Admin dashboard data +router.get('/dashboard', (req, res) => { + sendSuccess(res, { + admin: { + id: req.user._id, + fullName: req.user.fullName, + role: req.user.role + }, + stats: { + totalUsers: 0, + activeCampaigns: 0, + pendingVerifications: 0 + } + }, 200, 'Admin dashboard statistics retrieved'); +}); + +// GET /api/admin/users - List all users +router.get('/users', (req, res) => { + sendSuccess(res, { + users: [], + message: 'User management system placeholder' + }, 200, 'Users retrieved successfully'); +}); + +module.exports = router;