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.
This commit is contained in:
2026-04-26 22:06:28 +02:00
parent f1f913f63f
commit 61cdec00e7
7 changed files with 113 additions and 35 deletions
-9
View File
@@ -8,10 +8,6 @@ module.exports.createArticle = asyncHandler(async (req, res, next) => {
try { try {
const loggedUser = await UserService.getLoggedUserById(req.payload.id); 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 { title, description, body, tagList } = req.body.article;
const articleSlug = ArticleService.slugify(title); const articleSlug = ArticleService.slugify(title);
const slugInDB = await ArticleService.isSlugUsed(articleSlug); 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));
}
};
-18
View File
@@ -32,10 +32,6 @@ module.exports.authenticate = asyncHandler(async (req, res, next) => {
module.exports.createUser = asyncHandler(async (req, res, next) => { module.exports.createUser = asyncHandler(async (req, res, next) => {
try { 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 { username, firstname, lastname, phone, email, password } = req.body.user;
const userSlug = UserService.slugify(username); const userSlug = UserService.slugify(username);
const slugInDB = await UserService.isUsernameUsed(userSlug); 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) => { module.exports.loginAsUser = asyncHandler(async (req, res, next) => {
try { 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) { passport.authenticate('local', { session: false }, function (err, user, info) {
if (err) { if (err) {
return next(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.")); 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') { if (typeof req.body.user.email !== 'undefined') {
user.email = req.body.user.email; 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.")); 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') { if (typeof req.body.user.role !== 'undefined') {
user.role = req.body.user.role; 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.")); 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') { if (typeof req.body.user.username !== 'undefined') {
user.username = req.body.user.username; 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));
}
};
+11
View File
@@ -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;
@@ -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 };
@@ -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 };
+4 -2
View File
@@ -10,14 +10,16 @@ const {
deleteFavoriteArticle deleteFavoriteArticle
} = require('../../../controllers/articles.controller'); } = require('../../../controllers/articles.controller');
const auth = require('../../../middlewares/auth'); const auth = require('../../../middlewares/auth');
const validate = require('../../../middlewares/validate');
const { createArticleRules, updateArticleRules } = require('../../../middlewares/validators/articles.validators');
const articleRouter = express.Router(); const articleRouter = express.Router();
articleRouter.get('/', auth.optional, getArticles); 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('/feed', auth.required, articlesFeed);
articleRouter.get('/:slug', auth.optional, getArticle); 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.delete('/:slug', auth.required, deleteArticle);
articleRouter.post('/:slug/favorite', auth.required, addFavoriteArticle); articleRouter.post('/:slug/favorite', auth.required, addFavoriteArticle);
articleRouter.delete('/:slug/favorite', auth.required, deleteFavoriteArticle); articleRouter.delete('/:slug/favorite', auth.required, deleteFavoriteArticle);
+8 -6
View File
@@ -5,6 +5,8 @@ const {
updateUser, updateUserEmail, updateUserPassword, updateUserRole, updateUserUsername updateUser, updateUserEmail, updateUserPassword, updateUserRole, updateUserUsername
} = require('../../../controllers/users.controller'); } = require('../../../controllers/users.controller');
const auth = require('../../../middlewares/auth'); 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(); const userRouter = express.Router();
@@ -17,15 +19,15 @@ const authLimiter = rateLimit({
}); });
userRouter.get('/', auth.required, getUser); userRouter.get('/', auth.required, getUser);
userRouter.put('/', auth.required, updateUser); userRouter.put('/', auth.required, updateUserRules, validate, updateUser);
userRouter.post('/', authLimiter, auth.optional, createUser); userRouter.post('/', authLimiter, auth.optional, createUserRules, validate, createUser);
userRouter.get('/authenticate', auth.optional, authenticate); 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.post('/register', auth.optional, createUser);
//userRouter.get('/users', auth.required, getAllUsers); //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/password', auth.required, updateUserPassword);
userRouter.put('/update/role', auth.required, updateUserRole); userRouter.put('/update/role', auth.required, updateRoleRules, validate, updateUserRole);
userRouter.put('/update/username', auth.required, updateUserUsername); userRouter.put('/update/username', auth.required, updateUsernameRules, validate, updateUserUsername);
module.exports = userRouter; module.exports = userRouter;