diff --git a/package-lock.json b/package-lock.json index 3463871..aecf775 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,5 +1,5 @@ { - "name": "RBAC", + "name": "rbac", "lockfileVersion": 3, "requires": true, "packages": { @@ -10,6 +10,7 @@ "cors": "^2.8.5", "dotenv": "^17.2.3", "express": "^5.1.0", + "joi": "^18.0.1", "jsonwebtoken": "^9.0.2", "mongoose": "^8.19.1", "nodemon": "^3.1.10", @@ -551,6 +552,54 @@ "node": "^18.18.0 || ^20.9.0 || >=21.1.0" } }, + "node_modules/@hapi/address": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/@hapi/address/-/address-5.1.1.tgz", + "integrity": "sha512-A+po2d/dVoY7cYajycYI43ZbYMXukuopIsqCjh5QzsBCipDtdofHntljDlpccMjIfTy6UOkg+5KPriwYch2bXA==", + "license": "BSD-3-Clause", + "dependencies": { + "@hapi/hoek": "^11.0.2" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/@hapi/formula": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/@hapi/formula/-/formula-3.0.2.tgz", + "integrity": "sha512-hY5YPNXzw1He7s0iqkRQi+uMGh383CGdyyIGYtB+W5N3KHPXoqychklvHhKCC9M3Xtv0OCs/IHw+r4dcHtBYWw==", + "license": "BSD-3-Clause" + }, + "node_modules/@hapi/hoek": { + "version": "11.0.7", + "resolved": "https://registry.npmjs.org/@hapi/hoek/-/hoek-11.0.7.tgz", + "integrity": "sha512-HV5undWkKzcB4RZUusqOpcgxOaq6VOAH7zhhIr2g3G8NF/MlFO75SjOr2NfuSx0Mh40+1FqCkagKLJRykUWoFQ==", + "license": "BSD-3-Clause" + }, + "node_modules/@hapi/pinpoint": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/@hapi/pinpoint/-/pinpoint-2.0.1.tgz", + "integrity": "sha512-EKQmr16tM8s16vTT3cA5L0kZZcTMU5DUOZTuvpnY738m+jyP3JIUj+Mm1xc1rsLkGBQ/gVnfKYPwOmPg1tUR4Q==", + "license": "BSD-3-Clause" + }, + "node_modules/@hapi/tlds": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/@hapi/tlds/-/tlds-1.1.3.tgz", + "integrity": "sha512-QIvUMB5VZ8HMLZF9A2oWr3AFM430QC8oGd0L35y2jHpuW6bIIca6x/xL7zUf4J7L9WJ3qjz+iJII8ncaeMbpSg==", + "license": "BSD-3-Clause", + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/@hapi/topo": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/@hapi/topo/-/topo-6.0.2.tgz", + "integrity": "sha512-KR3rD5inZbGMrHmgPxsJ9dbi6zEK+C3ZwUwTa+eMwWLz7oijWUTWD2pMSNNYJAU6Qq+65NkxXjqHr/7LM2Xkqg==", + "license": "BSD-3-Clause", + "dependencies": { + "@hapi/hoek": "^11.0.2" + } + }, "node_modules/@humanfs/core": { "version": "0.19.1", "resolved": "https://registry.npmjs.org/@humanfs/core/-/core-0.19.1.tgz", @@ -612,6 +661,12 @@ "sparse-bitfield": "^3.0.3" } }, + "node_modules/@standard-schema/spec": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/@standard-schema/spec/-/spec-1.0.0.tgz", + "integrity": "sha512-m2bOd0f2RT9k8QJx1JN85cZYyH1RqFBdlwtkSlf4tBDYLCiiZnv1fIIwacK6cqwXavOydf0NPToMQgpKq+dVlA==", + "license": "MIT" + }, "node_modules/@types/conventional-commits-parser": { "version": "5.0.0", "resolved": "https://registry.npmjs.org/@types/conventional-commits-parser/-/conventional-commits-parser-5.0.0.tgz", @@ -2298,6 +2353,24 @@ "jiti": "bin/jiti.js" } }, + "node_modules/joi": { + "version": "18.0.1", + "resolved": "https://registry.npmjs.org/joi/-/joi-18.0.1.tgz", + "integrity": "sha512-IiQpRyypSnLisQf3PwuN2eIHAsAIGZIrLZkd4zdvIar2bDyhM91ubRjy8a3eYablXsh9BeI/c7dmPYHca5qtoA==", + "license": "BSD-3-Clause", + "dependencies": { + "@hapi/address": "^5.1.1", + "@hapi/formula": "^3.0.2", + "@hapi/hoek": "^11.0.7", + "@hapi/pinpoint": "^2.0.1", + "@hapi/tlds": "^1.1.1", + "@hapi/topo": "^6.0.2", + "@standard-schema/spec": "^1.0.0" + }, + "engines": { + "node": ">= 20" + } + }, "node_modules/js-tokens": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", diff --git a/package.json b/package.json index b5ca97c..adf4c0f 100644 --- a/package.json +++ b/package.json @@ -23,6 +23,7 @@ "cors": "^2.8.5", "dotenv": "^17.2.3", "express": "^5.1.0", + "joi": "^18.0.1", "jsonwebtoken": "^9.0.2", "mongoose": "^8.19.1", "nodemon": "^3.1.10", diff --git a/src/middlewares/validate.middleware.js b/src/middlewares/validate.middleware.js new file mode 100644 index 0000000..e07f432 --- /dev/null +++ b/src/middlewares/validate.middleware.js @@ -0,0 +1,11 @@ +export const validateMiddleware = (schema, property = 'body') => { + return (req, res, next) => { + const { error } = schema.validate(req[property]); + if (error) { + return res.status(400).json({ + error: error.details[0].message, + }); + } + next(); + }; +}; diff --git a/src/routes/authRoutes.js b/src/routes/authRoutes.js index 669790d..eba67c6 100644 --- a/src/routes/authRoutes.js +++ b/src/routes/authRoutes.js @@ -1,11 +1,13 @@ import express from 'express'; -import { registerUser,loginUser, forgotPassword, resetPassword } from '../controllers/authController.js'; -import { authMiddleware } from '../middlewares/auth.middleware.js'; +import { registerUser,loginUser } from '../controllers/authController.js'; +import { registerSchema, loginSchema } from '../validations/authValidation.js'; +import {validateMiddleware} from '../middlewares/validate.middleware.js'; + const router = express.Router(); -router.post('/register', registerUser); -router.post('/login', loginUser); +router.post('/register', validateMiddleware(registerSchema), registerUser); +router.post('/login', validateMiddleware(loginSchema), loginUser); router.post('/forgotPassword',forgotPassword); router.post('/resetPassword/:token',resetPassword); diff --git a/src/routes/rbacRoutes.js b/src/routes/rbacRoutes.js index fc8b328..890e556 100644 --- a/src/routes/rbacRoutes.js +++ b/src/routes/rbacRoutes.js @@ -1,16 +1,18 @@ import express from 'express'; import { authMiddleware } from '../middlewares/auth.middleware.js'; import { checkRole } from '../middlewares/rbac.middleware.js'; +import { adminAccessSchema, userAccessSchema } from '../validations/rbacValidation.js'; +import {validateMiddleware} from '../middlewares/validate.middleware.js'; const router = express.Router(); -router.get('/admin-only', authMiddleware, checkRole(['Admin']), (req, res) => { +router.get('/admin-only', validateMiddleware(adminAccessSchema, 'headers'), authMiddleware, checkRole(['Admin']), (req, res) => { return res.status(200).json({ message: 'Welcome, Admin' }); }); - -router.get('/user-only', authMiddleware, checkRole(['User']), (req, res) => { + +router.get('/user-only', validateMiddleware(userAccessSchema, 'headers'), authMiddleware, checkRole(['User']), (req, res) => { return res.status(200).json({ message: 'Welcome, User' }); }); diff --git a/src/validations/authValidation.js b/src/validations/authValidation.js new file mode 100644 index 0000000..d311357 --- /dev/null +++ b/src/validations/authValidation.js @@ -0,0 +1,35 @@ +import Joi from 'joi'; + +const registerSchema = Joi.object({ + username: Joi.string().min(3).max(30).required().messages({ + 'string.empty': 'Username is required', + 'string.min': 'Username must be at least 3 characters', + 'string.max': 'Username must be at most 30 characters', + }), + fullname: Joi.string().min(3).max(50).required().messages({ + 'string.empty': 'Full name is required', + 'string.min': 'Full name must be at least 3 characters', + 'string.max': 'Full name must be at most 50 characters', + }), + email: Joi.string().email().required().messages({ + 'string.empty': 'Email is required', + 'string.email': 'Email must be a valid email address', + }), + password: Joi.string().min(6).required().messages({ + 'string.empty': 'Password is required', + 'string.min': 'Password must be at least 6 characters', + }), +}); + +const loginSchema = Joi.object({ + email: Joi.string().email().required().messages({ + 'string.empty': 'Email is required', + 'string.email': 'Email must be a valid email address', + }), + password: Joi.string().min(6).required().messages({ + 'string.empty': 'Password is required', + 'string.min': 'Password must be at least 6 characters', + }), +}); + +export { registerSchema, loginSchema }; diff --git a/src/validations/rbacValidation.js b/src/validations/rbacValidation.js new file mode 100644 index 0000000..26e1a1e --- /dev/null +++ b/src/validations/rbacValidation.js @@ -0,0 +1,24 @@ +// src/validations/rbacValidation.js +import Joi from "joi"; + +const adminAccessSchema = Joi.object({ + authorization: Joi.string() + .pattern(/^Bearer\s.+$/) + .required() + .messages({ + "string.pattern.base": "Authorization header must be in Bearer token format", + "string.empty": "Authorization token is required", + }), +}).unknown(true); // allows other headers + +const userAccessSchema = Joi.object({ + authorization: Joi.string() + .pattern(/^Bearer\s.+$/) + .required() + .messages({ + "string.pattern.base": "Authorization header must be in Bearer token format", + "string.empty": "Authorization token is required", + }), +}).unknown(true); + +export { adminAccessSchema, userAccessSchema };