Skip to content

Commit

Permalink
Validate request body with ajv (#131)
Browse files Browse the repository at this point in the history
* Fix withUser of undefined

* Add validation using ajv

* fix
  • Loading branch information
hoangvvo authored Sep 22, 2021
1 parent 0362d04 commit 4f4f28a
Show file tree
Hide file tree
Showing 12 changed files with 261 additions and 165 deletions.
3 changes: 2 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -92,7 +92,8 @@ This project uses the following dependencies:
- `next-connect` - recommended if you want to use Express/Connect middleware and easier method routing.
- `next-session`, `connect-mongo` - required for session, may be replaced with other session libraries such as `cookie-session`, `next-iron-session`, or `express-session` (`express-session` is observed not to work properly on Next.js 11+).
- `bcryptjs` - optional, may be replaced with any password-hashing library. `argon2` recommended.
- `validator` - optional but recommended.
- `validator` - optional but recommended, to validate email.
- `ajv` - optional but recommended, to validate request body.
- `multer` - may be replaced with any middleware that handles `multipart/form-data`
- `cloudinary` - optional, **only if** you are using [Cloudinary](https://cloudinary.com) for image upload.

Expand Down
15 changes: 15 additions & 0 deletions api-lib/constants.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
export const ValidateProps = {
user: {
username: { type: 'string', minLength: 4, maxLength: 20 },
name: { type: 'string', minLength: 1, maxLength: 50 },
password: { type: 'string', minLength: 8 },
email: { type: 'string', minLength: 1 },
bio: { type: 'string', minLength: 0, maxLength: 160 },
},
post: {
content: { type: 'string', minLength: 1, maxLength: 280 },
},
comment: {
content: { type: 'string', minLength: 1, maxLength: 280 },
},
};
2 changes: 1 addition & 1 deletion api-lib/db/post.js
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { nanoid } from 'nanoid';
import { dbProjectionUsers } from './user';

export async function findPostById(db, id) {
export async function findPostById(db, id, withUser) {
const post = await db.collection('posts').findOne({ _id: id });
if (!post) return null;
post.creator = await db
Expand Down
18 changes: 18 additions & 0 deletions api-lib/middlewares/ajv.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
import Ajv from 'ajv';

export function validateBody(schema) {
const ajv = new Ajv();
const validate = ajv.compile(schema);
return (req, res, next) => {
const valid = validate(req.body);
if (valid) {
return next();
} else {
console.log(validate.errors);
const error = validate.errors[0];
return res
.status(400)
.end(`"${error.instancePath.substring(1)}" ${error.message}`);
}
};
}
1 change: 1 addition & 0 deletions api-lib/middlewares/index.js
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
export { validateBody } from './ajv';
export { default as all } from './all';
export { default as auth } from './auth';
export { default as database } from './database';
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@
},
"homepage": "https://github.com/hoangvvo/nextjs-mongodb-app#readme",
"dependencies": {
"ajv": "^8.6.3",
"bcryptjs": "^2.4.3",
"cloudinary": "^1.27.0",
"connect-mongo": "^4.6.0",
Expand Down
54 changes: 31 additions & 23 deletions pages/api/posts/[postId]/comments/index.js
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import { ValidateProps } from '@/api-lib/constants';
import { findPostById } from '@/api-lib/db';
import { findComments, insertComment } from '@/api-lib/db/comment';
import { all } from '@/api-lib/middlewares';
import { all, validateBody } from '@/api-lib/middlewares';
import { ncOpts } from '@/api-lib/nc';
import nc from 'next-connect';

Expand All @@ -25,28 +26,35 @@ handler.get(async (req, res) => {
return res.send({ comments });
});

handler.post(async (req, res) => {
if (!req.user) {
return res.status(401).send('unauthenticated');
handler.post(
validateBody({
type: 'object',
properties: {
content: ValidateProps.comment.content,
},
required: ['content'],
additionalProperties: false,
}),
async (req, res) => {
if (!req.user) {
return res.status(401).send('unauthenticated');
}

const content = req.body.content;

const post = await findPostById(req.db, req.query.postId);

if (!post) {
return res.status(404).send('post not found');
}

const comment = await insertComment(req.db, post._id, {
creatorId: req.user._id,
content,
});

return res.json({ comment });
}

const content = req.body.content;
if (!content) {
return res.status(400).send('You must write something');
}

const post = await findPostById(req.db, req.query.postId);

if (!post) {
return res.status(404).send('post not found');
}

const comment = await insertComment(req.db, post._id, {
creatorId: req.user._id,
content,
});

return res.json({ comment });
});
);

export default handler;
38 changes: 23 additions & 15 deletions pages/api/posts/index.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import { ValidateProps } from '@/api-lib/constants';
import { findPosts, insertPost } from '@/api-lib/db';
import { all } from '@/api-lib/middlewares';
import { all, validateBody } from '@/api-lib/middlewares';
import { ncOpts } from '@/api-lib/nc';
import nc from 'next-connect';

Expand All @@ -18,20 +19,27 @@ handler.get(async (req, res) => {
res.send({ posts });
});

handler.post(async (req, res) => {
if (!req.user) {
return res.status(401).send('unauthenticated');
handler.post(
validateBody({
type: 'object',
properties: {
content: ValidateProps.post.content,
},
required: ['content'],
additionalProperties: false,
}),
async (req, res) => {
if (!req.user) {
return res.status(401).send('unauthenticated');
}

const post = await insertPost(req.db, {
content: req.body.content,
creatorId: req.user._id,
});

return res.json({ post });
}

if (!req.body.content)
return res.status(400).send('You must write something');

const post = await insertPost(req.db, {
content: req.body.content,
creatorId: req.user._id,
});

return res.json({ post });
});
);

export default handler;
58 changes: 35 additions & 23 deletions pages/api/user/index.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import { ValidateProps } from '@/api-lib/constants';
import { updateUserById } from '@/api-lib/db';
import { all } from '@/api-lib/middlewares';
import { all, validateBody } from '@/api-lib/middlewares';
import { ncOpts } from '@/api-lib/nc';
import { v2 as cloudinary } from 'cloudinary';
import multer from 'multer';
Expand Down Expand Up @@ -30,30 +31,41 @@ handler.get(async (req, res) => {
return res.json({ user: u });
});

handler.patch(upload.single('profilePicture'), async (req, res) => {
if (!req.user) {
req.status(401).end();
return;
}
let profilePicture;
if (req.file) {
const image = await cloudinary.uploader.upload(req.file.path, {
width: 512,
height: 512,
crop: 'fill',
});
profilePicture = image.secure_url;
}
const { name, bio } = req.body;
handler.patch(
validateBody({
type: 'object',
properties: {
name: ValidateProps.user.name,
bio: ValidateProps.user.bio,
},
additionalProperties: false,
}),
upload.single('profilePicture'),
async (req, res) => {
if (!req.user) {
req.status(401).end();
return;
}
let profilePicture;
if (req.file) {
const image = await cloudinary.uploader.upload(req.file.path, {
width: 512,
height: 512,
crop: 'fill',
});
profilePicture = image.secure_url;
}
const { name, bio } = req.body;

const user = await updateUserById(req.db, req.user._id, {
...(name && { name }),
...(typeof bio === 'string' && { bio }),
...(profilePicture && { profilePicture }),
});
const user = await updateUserById(req.db, req.user._id, {
...(name && { name }),
...(typeof bio === 'string' && { bio }),
...(profilePicture && { profilePicture }),
});

res.json({ user });
});
res.json({ user });
}
);

export const config = {
api: {
Expand Down
56 changes: 34 additions & 22 deletions pages/api/user/password/index.js
Original file line number Diff line number Diff line change
@@ -1,35 +1,47 @@
import { ValidateProps } from '@/api-lib/constants';
import { UNSAFE_findUserForAuth, updateUserById } from '@/api-lib/db';
import { all } from '@/api-lib/middlewares';
import { all, validateBody } from '@/api-lib/middlewares';
import { ncOpts } from '@/api-lib/nc';
import bcrypt from 'bcryptjs';
import nc from 'next-connect';

const handler = nc(ncOpts);
handler.use(all);

handler.put(async (req, res) => {
if (!req.user) {
res.json(401).send('you need to be authenticated');
return;
}
const { oldPassword, newPassword } = req.body;
handler.put(
validateBody({
type: 'object',
properties: {
oldPassword: ValidateProps.user.password,
newPassword: ValidateProps.user.password,
},
required: ['oldPassword', 'newPassword'],
additionalProperties: false,
}),
async (req, res) => {
if (!req.user) {
res.json(401).send('you need to be authenticated');
return;
}
const { oldPassword, newPassword } = req.body;

if (
!(await bcrypt.compare(
oldPassword,
(
await UNSAFE_findUserForAuth(req.db, req.user._id, true)
).password
))
) {
res.status(401).send('The password you has entered is incorrect.');
return;
}
const password = await bcrypt.hash(newPassword, 10);
if (
!(await bcrypt.compare(
oldPassword,
(
await UNSAFE_findUserForAuth(req.db, req.user._id, true)
).password
))
) {
res.status(401).send('The password you has entered is incorrect.');
return;
}
const password = await bcrypt.hash(newPassword, 10);

await updateUserById(req.db, req.user._id, { password });
await updateUserById(req.db, req.user._id, { password });

res.end('ok');
});
res.end('ok');
}
);

export default handler;
Loading

1 comment on commit 4f4f28a

@vercel
Copy link

@vercel vercel bot commented on 4f4f28a Sep 22, 2021

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please sign in to comment.