From 61cdec00e71ca5d37ee323de791963512a21d7f9 Mon Sep 17 00:00:00 2001 From: Julien Gautier Date: Sun, 26 Apr 2026 22:06:28 +0200 Subject: [PATCH] feat(validation): introduce express-validator on CMS user and article endpoints Replaces the broken fieldValidation helper (called next() without return, so execution continued on invalid input) with express-validator chains applied at the route level. Validation covers: email format, username constraints, password min length, field lengths, URL and phone formats. products and tags controllers retain their local fieldValidation pending a separate ecommerce validation pass. --- src/controllers/articles.controller.js | 9 --- src/controllers/users.controller.js | 18 ------ src/middlewares/validate.js | 11 ++++ .../validators/articles.validators.js | 27 ++++++++ .../validators/users.validators.js | 63 +++++++++++++++++++ src/routes/api/cms/articles.routes.js | 6 +- src/routes/api/cms/users.routes.js | 14 +++-- 7 files changed, 113 insertions(+), 35 deletions(-) create mode 100644 src/middlewares/validate.js create mode 100644 src/middlewares/validators/articles.validators.js create mode 100644 src/middlewares/validators/users.validators.js diff --git a/src/controllers/articles.controller.js b/src/controllers/articles.controller.js index 58fcd38..fabd1c9 100644 --- a/src/controllers/articles.controller.js +++ b/src/controllers/articles.controller.js @@ -8,10 +8,6 @@ module.exports.createArticle = asyncHandler(async (req, res, next) => { try { const loggedUser = await UserService.getLoggedUserById(req.payload.id); - fieldValidation(req.body.article.title, next); - fieldValidation(req.body.article.description, next); - fieldValidation(req.body.article.body, next); - const { title, description, body, tagList } = req.body.article; const articleSlug = ArticleService.slugify(title); const slugInDB = await ArticleService.isSlugUsed(articleSlug); @@ -254,8 +250,3 @@ module.exports.deleteFavoriteArticle = asyncHandler(async (req, res, next) => { } }); -const fieldValidation = (field, next) => { - if (!field) { - return next(new ErrorResponse(`Missing fields`, 400)); - } -}; diff --git a/src/controllers/users.controller.js b/src/controllers/users.controller.js index a133b6f..63e9649 100644 --- a/src/controllers/users.controller.js +++ b/src/controllers/users.controller.js @@ -32,10 +32,6 @@ module.exports.authenticate = asyncHandler(async (req, res, next) => { module.exports.createUser = asyncHandler(async (req, res, next) => { try { - fieldValidation(req.body.user.username, next); - fieldValidation(req.body.user.email, next); - fieldValidation(req.body.user.password, next); - const { username, firstname, lastname, phone, email, password } = req.body.user; const userSlug = UserService.slugify(username); const slugInDB = await UserService.isUsernameUsed(userSlug); @@ -92,12 +88,6 @@ module.exports.getUser = asyncHandler(async (req, res, next) => { module.exports.loginAsUser = asyncHandler(async (req, res, next) => { try { - if (!req.body.user.email) { - return res.status(422).json({ errors: { email: "email can't be blank" } }); - } - if (!req.body.user.password) { - return res.status(422).json({ errors: { password: " password can't be blank" } }); - } passport.authenticate('local', { session: false }, function (err, user, info) { if (err) { return next(err); @@ -161,7 +151,6 @@ module.exports.updateUserEmail = asyncHandler(async (req, res, next) => { return next(new ErrorResponse("Unauthorized", 401, "You are not allowed to access this resource.")); } - fieldValidation(req.body.user.email, next); if (typeof req.body.user.email !== 'undefined') { user.email = req.body.user.email; } @@ -205,7 +194,6 @@ module.exports.updateUserRole = asyncHandler(async (req, res, next) => { return next(new ErrorResponse("Forbidden", 403, "You are not allowed to change user roles.")); } - fieldValidation(req.body.user.role, next); if (typeof req.body.user.role !== 'undefined') { user.role = req.body.user.role; } @@ -226,7 +214,6 @@ module.exports.updateUserUsername = asyncHandler(async (req, res, next) => { return next(new ErrorResponse("Unauthorized", 401, "You are not allowed to access this resource.")); } - fieldValidation(req.body.user.username, next); if (typeof req.body.user.username !== 'undefined') { user.username = req.body.user.username; } @@ -240,8 +227,3 @@ module.exports.updateUserUsername = asyncHandler(async (req, res, next) => { } }); -const fieldValidation = (field, next) => { - if (!field) { - return next(new ErrorResponse(`Missing fields`, 400)); - } -}; diff --git a/src/middlewares/validate.js b/src/middlewares/validate.js new file mode 100644 index 0000000..146ede4 --- /dev/null +++ b/src/middlewares/validate.js @@ -0,0 +1,11 @@ +const { validationResult } = require('express-validator'); + +const validate = (req, res, next) => { + const errors = validationResult(req); + if (!errors.isEmpty()) { + return res.status(422).json({ errors: errors.mapped() }); + } + return next(); +}; + +module.exports = validate; diff --git a/src/middlewares/validators/articles.validators.js b/src/middlewares/validators/articles.validators.js new file mode 100644 index 0000000..b47f426 --- /dev/null +++ b/src/middlewares/validators/articles.validators.js @@ -0,0 +1,27 @@ +const { body } = require('express-validator'); + +const createArticleRules = [ + body('article.title') + .notEmpty().withMessage("can't be blank") + .isLength({ max: 255 }).withMessage('must be at most 255 characters') + .trim(), + body('article.description') + .notEmpty().withMessage("can't be blank") + .isLength({ max: 1000 }).withMessage('must be at most 1000 characters') + .trim(), + body('article.body') + .notEmpty().withMessage("can't be blank"), +]; + +const updateArticleRules = [ + body('article.title') + .optional() + .isLength({ max: 255 }).withMessage('must be at most 255 characters') + .trim(), + body('article.description') + .optional() + .isLength({ max: 1000 }).withMessage('must be at most 1000 characters') + .trim(), +]; + +module.exports = { createArticleRules, updateArticleRules }; diff --git a/src/middlewares/validators/users.validators.js b/src/middlewares/validators/users.validators.js new file mode 100644 index 0000000..1eff2d6 --- /dev/null +++ b/src/middlewares/validators/users.validators.js @@ -0,0 +1,63 @@ +const { body } = require('express-validator'); + +const createUserRules = [ + body('user.email') + .notEmpty().withMessage("can't be blank") + .isEmail().withMessage('must be a valid email address') + .normalizeEmail(), + body('user.username') + .notEmpty().withMessage("can't be blank") + .isLength({ min: 3, max: 50 }).withMessage('must be between 3 and 50 characters') + .matches(/^[a-zA-Z0-9_.-]+$/).withMessage('must contain only letters, numbers, underscores, dots or hyphens'), + body('user.password') + .notEmpty().withMessage("can't be blank") + .isLength({ min: 8 }).withMessage('must be at least 8 characters'), +]; + +const loginRules = [ + body('user.email') + .notEmpty().withMessage("can't be blank") + .isEmail().withMessage('must be a valid email address') + .normalizeEmail(), + body('user.password') + .notEmpty().withMessage("can't be blank"), +]; + +const updateUserRules = [ + body('user.firstname') + .optional() + .isLength({ max: 100 }).withMessage('must be at most 100 characters') + .trim(), + body('user.lastname') + .optional() + .isLength({ max: 100 }).withMessage('must be at most 100 characters') + .trim(), + body('user.phone') + .optional() + .isMobilePhone().withMessage('must be a valid phone number'), + body('user.image') + .optional() + .isURL().withMessage('must be a valid URL'), +]; + +const updateEmailRules = [ + body('user.email') + .notEmpty().withMessage("can't be blank") + .isEmail().withMessage('must be a valid email address') + .normalizeEmail(), +]; + +const updateUsernameRules = [ + body('user.username') + .notEmpty().withMessage("can't be blank") + .isLength({ min: 3, max: 50 }).withMessage('must be between 3 and 50 characters') + .matches(/^[a-zA-Z0-9_.-]+$/).withMessage('must contain only letters, numbers, underscores, dots or hyphens'), +]; + +const updateRoleRules = [ + body('user.role') + .notEmpty().withMessage("can't be blank") + .isIn(['Admin', 'User']).withMessage('must be Admin or User'), +]; + +module.exports = { createUserRules, loginRules, updateUserRules, updateEmailRules, updateUsernameRules, updateRoleRules }; diff --git a/src/routes/api/cms/articles.routes.js b/src/routes/api/cms/articles.routes.js index 7b670ca..bb2b5af 100644 --- a/src/routes/api/cms/articles.routes.js +++ b/src/routes/api/cms/articles.routes.js @@ -10,14 +10,16 @@ const { deleteFavoriteArticle } = require('../../../controllers/articles.controller'); const auth = require('../../../middlewares/auth'); +const validate = require('../../../middlewares/validate'); +const { createArticleRules, updateArticleRules } = require('../../../middlewares/validators/articles.validators'); const articleRouter = express.Router(); articleRouter.get('/', auth.optional, getArticles); -articleRouter.post('/', auth.required, createArticle); +articleRouter.post('/', auth.required, createArticleRules, validate, createArticle); articleRouter.get('/feed', auth.required, articlesFeed); articleRouter.get('/:slug', auth.optional, getArticle); -articleRouter.put('/:slug', auth.required, updateArticle); +articleRouter.put('/:slug', auth.required, updateArticleRules, validate, updateArticle); articleRouter.delete('/:slug', auth.required, deleteArticle); articleRouter.post('/:slug/favorite', auth.required, addFavoriteArticle); articleRouter.delete('/:slug/favorite', auth.required, deleteFavoriteArticle); diff --git a/src/routes/api/cms/users.routes.js b/src/routes/api/cms/users.routes.js index 813d1b1..a309f73 100644 --- a/src/routes/api/cms/users.routes.js +++ b/src/routes/api/cms/users.routes.js @@ -5,6 +5,8 @@ const { updateUser, updateUserEmail, updateUserPassword, updateUserRole, updateUserUsername } = require('../../../controllers/users.controller'); const auth = require('../../../middlewares/auth'); +const validate = require('../../../middlewares/validate'); +const { createUserRules, loginRules, updateUserRules, updateEmailRules, updateUsernameRules, updateRoleRules } = require('../../../middlewares/validators/users.validators'); const userRouter = express.Router(); @@ -17,15 +19,15 @@ const authLimiter = rateLimit({ }); userRouter.get('/', auth.required, getUser); -userRouter.put('/', auth.required, updateUser); -userRouter.post('/', authLimiter, auth.optional, createUser); +userRouter.put('/', auth.required, updateUserRules, validate, updateUser); +userRouter.post('/', authLimiter, auth.optional, createUserRules, validate, createUser); userRouter.get('/authenticate', auth.optional, authenticate); -userRouter.post('/login', authLimiter, auth.optional, loginAsUser); +userRouter.post('/login', authLimiter, auth.optional, loginRules, validate, loginAsUser); //userRouter.post('/register', auth.optional, createUser); //userRouter.get('/users', auth.required, getAllUsers); -userRouter.put('/update/email', auth.required, updateUserEmail); +userRouter.put('/update/email', auth.required, updateEmailRules, validate, updateUserEmail); userRouter.put('/update/password', auth.required, updateUserPassword); -userRouter.put('/update/role', auth.required, updateUserRole); -userRouter.put('/update/username', auth.required, updateUserUsername); +userRouter.put('/update/role', auth.required, updateRoleRules, validate, updateUserRole); +userRouter.put('/update/username', auth.required, updateUsernameRules, validate, updateUserUsername); module.exports = userRouter;