From 67efa9352867795db8fdc8ec34cb2d2cffbc9ffc Mon Sep 17 00:00:00 2001 From: Hoang Vo Date: Sat, 11 Jun 2022 02:25:17 +0700 Subject: [PATCH] remove database middleware pattern --- api-lib/auth/passport.js | 14 +++--- api-lib/mail.js | 1 - api-lib/middlewares/database.js | 50 ---------------------- api-lib/middlewares/index.js | 1 - api-lib/middlewares/session.js | 2 +- api-lib/mongodb.js | 46 ++++++++++++++++++++ pages/api/auth.js | 4 +- pages/api/posts/[postId]/comments/index.js | 17 +++++--- pages/api/posts/index.js | 13 +++--- pages/api/user/email/verify.js | 9 ++-- pages/api/user/index.js | 12 ++++-- pages/api/user/password/index.js | 10 +++-- pages/api/user/password/reset.js | 17 +++++--- pages/api/users/[userId]/index.js | 7 ++- pages/api/users/index.js | 13 +++--- pages/forget-password/[token].jsx | 7 ++- pages/user/[username]/index.jsx | 10 ++--- pages/user/[username]/post/[postId].jsx | 7 ++- pages/verify-email/[token].jsx | 12 ++---- 19 files changed, 130 insertions(+), 122 deletions(-) delete mode 100644 api-lib/middlewares/database.js create mode 100644 api-lib/mongodb.js diff --git a/api-lib/auth/passport.js b/api-lib/auth/passport.js index e0c7adf..4e12af7 100644 --- a/api-lib/auth/passport.js +++ b/api-lib/auth/passport.js @@ -1,23 +1,27 @@ import { findUserForAuth, findUserWithEmailAndPassword } from '@/api-lib/db'; import passport from 'passport'; import { Strategy as LocalStrategy } from 'passport-local'; +import { getMongoDb } from '../mongodb'; passport.serializeUser((user, done) => { done(null, user._id); }); passport.deserializeUser((req, id, done) => { - findUserForAuth(req.db, id).then( - (user) => done(null, user), - (err) => done(err) - ); + getMongoDb().then((db) => { + findUserForAuth(db, id).then( + (user) => done(null, user), + (err) => done(err) + ); + }); }); passport.use( new LocalStrategy( { usernameField: 'email', passReqToCallback: true }, async (req, email, password, done) => { - const user = await findUserWithEmailAndPassword(req.db, email, password); + const db = await getMongoDb(); + const user = await findUserWithEmailAndPassword(db, email, password); if (user) done(null, user); else done(null, false, { message: 'Email or password is incorrect' }); } diff --git a/api-lib/mail.js b/api-lib/mail.js index fe45094..122fe1b 100644 --- a/api-lib/mail.js +++ b/api-lib/mail.js @@ -18,7 +18,6 @@ export async function sendMail({ from, to, subject, html }) { html, }); } catch (e) { - console.error(e); throw new Error(`Could not send email: ${e.message}`); } } diff --git a/api-lib/middlewares/database.js b/api-lib/middlewares/database.js deleted file mode 100644 index ad54043..0000000 --- a/api-lib/middlewares/database.js +++ /dev/null @@ -1,50 +0,0 @@ -import { MongoClient } from 'mongodb'; - -/** - * Global is used here to maintain a cached connection across hot reloads - * in development. This prevents connections growing exponentiatlly - * during API Route usage. - * https://github.com/vercel/next.js/pull/17666 - */ -global.mongo = global.mongo || {}; - -let indexesCreated = false; -async function createIndexes(db) { - await Promise.all([ - db - .collection('tokens') - .createIndex({ expireAt: -1 }, { expireAfterSeconds: 0 }), - db - .collection('posts') - .createIndexes([{ key: { createdAt: -1 } }, { key: { creatorId: -1 } }]), - db - .collection('comments') - .createIndexes([{ key: { createdAt: -1 } }, { key: { postId: -1 } }]), - db.collection('users').createIndexes([ - { key: { email: 1 }, unique: true }, - { key: { username: 1 }, unique: true }, - ]), - ]); - indexesCreated = true; -} - -export async function getMongoClient() { - if (!global.mongo.client) { - global.mongo.client = new MongoClient(process.env.MONGODB_URI); - } - // It is okay to call connect() even if it is connected - // using node-mongodb-native v4 (it will be no-op) - // See: https://github.com/mongodb/node-mongodb-native/blob/4.0/docs/CHANGES_4.0.0.md - await global.mongo.client.connect(); - return global.mongo.client; -} - -export default async function database(req, res, next) { - if (!global.mongo.client) { - global.mongo.client = new MongoClient(process.env.MONGODB_URI); - } - req.dbClient = await getMongoClient(); - req.db = req.dbClient.db(); // this use the database specified in the MONGODB_URI (after the "/") - if (!indexesCreated) await createIndexes(req.db); - return next(); -} diff --git a/api-lib/middlewares/index.js b/api-lib/middlewares/index.js index 0857b01..bd32262 100644 --- a/api-lib/middlewares/index.js +++ b/api-lib/middlewares/index.js @@ -1,3 +1,2 @@ export { validateBody } from './ajv'; export { default as auths } from './auth'; -export { default as database } from './database'; diff --git a/api-lib/middlewares/session.js b/api-lib/middlewares/session.js index 25e6b1d..21a2b64 100644 --- a/api-lib/middlewares/session.js +++ b/api-lib/middlewares/session.js @@ -1,7 +1,7 @@ +import { getMongoClient } from '@/api-lib/mongodb'; import MongoStore from 'connect-mongo'; import nextSession from 'next-session'; import { promisifyStore } from 'next-session/lib/compat'; -import { getMongoClient } from './database'; const mongoStore = MongoStore.create({ clientPromise: getMongoClient(), diff --git a/api-lib/mongodb.js b/api-lib/mongodb.js new file mode 100644 index 0000000..3ee7951 --- /dev/null +++ b/api-lib/mongodb.js @@ -0,0 +1,46 @@ +import { MongoClient } from 'mongodb'; + +let indexesCreated = false; +async function createIndexes(client) { + if (indexesCreated) return client; + const db = client.db(); + await Promise.all([ + db + .collection('tokens') + .createIndex({ expireAt: -1 }, { expireAfterSeconds: 0 }), + db + .collection('posts') + .createIndexes([{ key: { createdAt: -1 } }, { key: { creatorId: -1 } }]), + db + .collection('comments') + .createIndexes([{ key: { createdAt: -1 } }, { key: { postId: -1 } }]), + db.collection('users').createIndexes([ + { key: { email: 1 }, unique: true }, + { key: { username: 1 }, unique: true }, + ]), + ]); + indexesCreated = true; + return client; +} + +export async function getMongoClient() { + /** + * Global is used here to maintain a cached connection across hot reloads + * in development. This prevents connections growing exponentiatlly + * during API Route usage. + * https://github.com/vercel/next.js/pull/17666 + */ + if (!global.mongoClientPromise) { + const client = new MongoClient(process.env.MONGODB_URI); + // client.connect() returns an instance of MongoClient when resolved + global.mongoClientPromise = client + .connect() + .then((client) => createIndexes(client)); + } + return global.mongoClientPromise; +} + +export async function getMongoDb() { + const mongoClient = await getMongoClient(); + return mongoClient.db(); +} diff --git a/pages/api/auth.js b/pages/api/auth.js index 6bb16e6..e39273d 100644 --- a/pages/api/auth.js +++ b/pages/api/auth.js @@ -1,11 +1,11 @@ import { passport } from '@/api-lib/auth'; -import { auths, database } from '@/api-lib/middlewares'; +import { auths } from '@/api-lib/middlewares'; import { ncOpts } from '@/api-lib/nc'; import nc from 'next-connect'; const handler = nc(ncOpts); -handler.use(database, ...auths); +handler.use(...auths); handler.post(passport.authenticate('local'), (req, res) => { res.json({ user: req.user }); diff --git a/pages/api/posts/[postId]/comments/index.js b/pages/api/posts/[postId]/comments/index.js index 2d40606..c338c72 100644 --- a/pages/api/posts/[postId]/comments/index.js +++ b/pages/api/posts/[postId]/comments/index.js @@ -1,23 +1,24 @@ import { ValidateProps } from '@/api-lib/constants'; import { findPostById } from '@/api-lib/db'; import { findComments, insertComment } from '@/api-lib/db/comment'; -import { auths, database, validateBody } from '@/api-lib/middlewares'; +import { auths, validateBody } from '@/api-lib/middlewares'; +import { getMongoDb } from '@/api-lib/mongodb'; import { ncOpts } from '@/api-lib/nc'; import nc from 'next-connect'; const handler = nc(ncOpts); -handler.use(database); - handler.get(async (req, res) => { - const post = await findPostById(req.db, req.query.postId); + const db = await getMongoDb(); + + const post = await findPostById(db, req.query.postId); if (!post) { return res.status(404).json({ error: { message: 'Post is not found.' } }); } const comments = await findComments( - req.db, + db, req.query.postId, req.query.before ? new Date(req.query.before) : undefined, req.query.limit ? parseInt(req.query.limit, 10) : undefined @@ -41,15 +42,17 @@ handler.post( return res.status(401).end(); } + const db = await getMongoDb(); + const content = req.body.content; - const post = await findPostById(req.db, req.query.postId); + const post = await findPostById(db, req.query.postId); if (!post) { return res.status(404).json({ error: { message: 'Post is not found.' } }); } - const comment = await insertComment(req.db, post._id, { + const comment = await insertComment(db, post._id, { creatorId: req.user._id, content, }); diff --git a/pages/api/posts/index.js b/pages/api/posts/index.js index f3f57ee..63b1507 100644 --- a/pages/api/posts/index.js +++ b/pages/api/posts/index.js @@ -1,16 +1,17 @@ import { ValidateProps } from '@/api-lib/constants'; import { findPosts, insertPost } from '@/api-lib/db'; -import { auths, database, validateBody } from '@/api-lib/middlewares'; +import { auths, validateBody } from '@/api-lib/middlewares'; +import { getMongoDb } from '@/api-lib/mongodb'; import { ncOpts } from '@/api-lib/nc'; import nc from 'next-connect'; const handler = nc(ncOpts); -handler.use(database); - handler.get(async (req, res) => { + const db = await getMongoDb(); + const posts = await findPosts( - req.db, + db, req.query.before ? new Date(req.query.before) : undefined, req.query.by, req.query.limit ? parseInt(req.query.limit, 10) : undefined @@ -34,7 +35,9 @@ handler.post( return res.status(401).end(); } - const post = await insertPost(req.db, { + const db = await getMongoDb(); + + const post = await insertPost(db, { content: req.body.content, creatorId: req.user._id, }); diff --git a/pages/api/user/email/verify.js b/pages/api/user/email/verify.js index d83fd8a..7b8f8e9 100644 --- a/pages/api/user/email/verify.js +++ b/pages/api/user/email/verify.js @@ -1,12 +1,13 @@ import { createToken } from '@/api-lib/db'; import { CONFIG as MAIL_CONFIG, sendMail } from '@/api-lib/mail'; -import { auths, database } from '@/api-lib/middlewares'; +import { auths } from '@/api-lib/middlewares'; +import { getMongoDb } from '@/api-lib/mongodb'; import { ncOpts } from '@/api-lib/nc'; import nc from 'next-connect'; const handler = nc(ncOpts); -handler.use(database, ...auths); +handler.use(...auths); handler.post(async (req, res) => { if (!req.user) { @@ -14,7 +15,9 @@ handler.post(async (req, res) => { return; } - const token = await createToken(req.db, { + const db = await getMongoDb(); + + const token = await createToken(db, { creatorId: req.user._id, type: 'emailVerify', expireAt: new Date(Date.now() + 1000 * 60 * 60 * 24), diff --git a/pages/api/user/index.js b/pages/api/user/index.js index bd59f03..7b63aed 100644 --- a/pages/api/user/index.js +++ b/pages/api/user/index.js @@ -1,6 +1,7 @@ import { ValidateProps } from '@/api-lib/constants'; import { findUserByUsername, updateUserById } from '@/api-lib/db'; -import { auths, database, validateBody } from '@/api-lib/middlewares'; +import { auths, validateBody } from '@/api-lib/middlewares'; +import { getMongoDb } from '@/api-lib/mongodb'; import { ncOpts } from '@/api-lib/nc'; import { slugUsername } from '@/lib/user'; import { v2 as cloudinary } from 'cloudinary'; @@ -24,7 +25,7 @@ if (process.env.CLOUDINARY_URL) { }); } -handler.use(database, ...auths); +handler.use(...auths); handler.get(async (req, res) => { if (!req.user) return res.json({ user: null }); @@ -47,6 +48,9 @@ handler.patch( req.status(401).end(); return; } + + const db = await getMongoDb(); + let profilePicture; if (req.file) { const image = await cloudinary.uploader.upload(req.file.path, { @@ -64,7 +68,7 @@ handler.patch( username = slugUsername(req.body.username); if ( username !== req.user.username && - (await findUserByUsername(req.db, username)) + (await findUserByUsername(db, username)) ) { res .status(403) @@ -73,7 +77,7 @@ handler.patch( } } - const user = await updateUserById(req.db, req.user._id, { + const user = await updateUserById(db, req.user._id, { ...(username && { username }), ...(name && { name }), ...(typeof bio === 'string' && { bio }), diff --git a/pages/api/user/password/index.js b/pages/api/user/password/index.js index e183ca2..4e18f9a 100644 --- a/pages/api/user/password/index.js +++ b/pages/api/user/password/index.js @@ -1,11 +1,12 @@ import { ValidateProps } from '@/api-lib/constants'; import { updateUserPasswordByOldPassword } from '@/api-lib/db'; -import { auths, database, validateBody } from '@/api-lib/middlewares'; +import { auths, validateBody } from '@/api-lib/middlewares'; +import { getMongoDb } from '@/api-lib/mongodb'; import { ncOpts } from '@/api-lib/nc'; import nc from 'next-connect'; const handler = nc(ncOpts); -handler.use(database, ...auths); +handler.use(...auths); handler.put( validateBody({ @@ -22,10 +23,13 @@ handler.put( res.json(401).end(); return; } + + const db = await getMongoDb(); + const { oldPassword, newPassword } = req.body; const success = await updateUserPasswordByOldPassword( - req.db, + db, req.user._id, oldPassword, newPassword diff --git a/pages/api/user/password/reset.js b/pages/api/user/password/reset.js index fba6e2c..41dc656 100644 --- a/pages/api/user/password/reset.js +++ b/pages/api/user/password/reset.js @@ -6,15 +6,14 @@ import { UNSAFE_updateUserPassword, } from '@/api-lib/db'; import { CONFIG as MAIL_CONFIG, sendMail } from '@/api-lib/mail'; -import { database, validateBody } from '@/api-lib/middlewares'; +import { validateBody } from '@/api-lib/middlewares'; +import { getMongoDb } from '@/api-lib/mongodb'; import { ncOpts } from '@/api-lib/nc'; import nc from 'next-connect'; import normalizeEmail from 'validator/lib/normalizeEmail'; const handler = nc(ncOpts); -handler.use(database); - handler.post( validateBody({ type: 'object', @@ -25,8 +24,10 @@ handler.post( additionalProperties: false, }), async (req, res) => { + const db = await getMongoDb(); + const email = normalizeEmail(req.body.email); - const user = await findUserByEmail(req.db, email); + const user = await findUserByEmail(db, email); if (!user) { res.status(400).json({ error: { message: 'We couldn’t find that email. Please try again.' }, @@ -34,7 +35,7 @@ handler.post( return; } - const token = await createToken(req.db, { + const token = await createToken(db, { creatorId: user._id, type: 'passwordReset', expireAt: new Date(Date.now() + 1000 * 60 * 20), @@ -67,8 +68,10 @@ handler.put( additionalProperties: false, }), async (req, res) => { + const db = await getMongoDb(); + const deletedToken = await findAndDeleteTokenByIdAndType( - req.db, + db, req.body.token, 'passwordReset' ); @@ -77,7 +80,7 @@ handler.put( return; } await UNSAFE_updateUserPassword( - req.db, + db, deletedToken.creatorId, req.body.password ); diff --git a/pages/api/users/[userId]/index.js b/pages/api/users/[userId]/index.js index a128acc..13e7ebd 100644 --- a/pages/api/users/[userId]/index.js +++ b/pages/api/users/[userId]/index.js @@ -1,14 +1,13 @@ import { findUserById } from '@/api-lib/db'; -import { database } from '@/api-lib/middlewares'; +import { getMongoDb } from '@/api-lib/mongodb'; import { ncOpts } from '@/api-lib/nc'; import nc from 'next-connect'; const handler = nc(ncOpts); -handler.use(database); - handler.get(async (req, res) => { - const user = await findUserById(req.db, req.query.userId); + const db = await getMongoDb(); + const user = await findUserById(db, req.query.userId); res.json({ user }); }); diff --git a/pages/api/users/index.js b/pages/api/users/index.js index 9b26a3b..8576df8 100644 --- a/pages/api/users/index.js +++ b/pages/api/users/index.js @@ -1,6 +1,7 @@ import { ValidateProps } from '@/api-lib/constants'; import { findUserByEmail, findUserByUsername, insertUser } from '@/api-lib/db'; -import { auths, database, validateBody } from '@/api-lib/middlewares'; +import { auths, validateBody } from '@/api-lib/middlewares'; +import { getMongoDb } from '@/api-lib/mongodb'; import { ncOpts } from '@/api-lib/nc'; import { slugUsername } from '@/lib/user'; import nc from 'next-connect'; @@ -9,8 +10,6 @@ import normalizeEmail from 'validator/lib/normalizeEmail'; const handler = nc(ncOpts); -handler.use(database); - handler.post( validateBody({ type: 'object', @@ -25,6 +24,8 @@ handler.post( }), ...auths, async (req, res) => { + const db = await getMongoDb(); + let { username, name, email, password } = req.body; username = slugUsername(req.body.username); email = normalizeEmail(req.body.email); @@ -34,19 +35,19 @@ handler.post( .json({ error: { message: 'The email you entered is invalid.' } }); return; } - if (await findUserByEmail(req.db, email)) { + if (await findUserByEmail(db, email)) { res .status(403) .json({ error: { message: 'The email has already been used.' } }); return; } - if (await findUserByUsername(req.db, username)) { + if (await findUserByUsername(db, username)) { res .status(403) .json({ error: { message: 'The username has already been taken.' } }); return; } - const user = await insertUser(req.db, { + const user = await insertUser(db, { email, originalPassword: password, bio: '', diff --git a/pages/forget-password/[token].jsx b/pages/forget-password/[token].jsx index 951443b..c9a356f 100644 --- a/pages/forget-password/[token].jsx +++ b/pages/forget-password/[token].jsx @@ -1,7 +1,6 @@ import { findTokenByIdAndType } from '@/api-lib/db'; -import { database } from '@/api-lib/middlewares'; +import { getMongoDb } from '@/api-lib/mongodb'; import { ForgetPasswordToken } from '@/page-components/ForgetPassword'; -import nc from 'next-connect'; import Head from 'next/head'; const ResetPasswordTokenPage = ({ valid, token }) => { @@ -16,10 +15,10 @@ const ResetPasswordTokenPage = ({ valid, token }) => { }; export async function getServerSideProps(context) { - await nc().use(database).run(context.req, context.res); + const db = await getMongoDb(); const tokenDoc = await findTokenByIdAndType( - context.req.db, + db, context.params.token, 'passwordReset' ); diff --git a/pages/user/[username]/index.jsx b/pages/user/[username]/index.jsx index 03ae533..57c2c5c 100644 --- a/pages/user/[username]/index.jsx +++ b/pages/user/[username]/index.jsx @@ -1,7 +1,6 @@ import { findUserByUsername } from '@/api-lib/db'; -import { database } from '@/api-lib/middlewares'; +import { getMongoDb } from '@/api-lib/mongodb'; import { User } from '@/page-components/User'; -import nc from 'next-connect'; import Head from 'next/head'; export default function UserPage({ user }) { @@ -18,12 +17,9 @@ export default function UserPage({ user }) { } export async function getServerSideProps(context) { - await nc().use(database).run(context.req, context.res); + const db = await getMongoDb(); - const user = await findUserByUsername( - context.req.db, - context.params.username - ); + const user = await findUserByUsername(db, context.params.username); if (!user) { return { notFound: true, diff --git a/pages/user/[username]/post/[postId].jsx b/pages/user/[username]/post/[postId].jsx index 135db64..8dbfcc4 100644 --- a/pages/user/[username]/post/[postId].jsx +++ b/pages/user/[username]/post/[postId].jsx @@ -1,7 +1,6 @@ import { findPostById } from '@/api-lib/db'; -import { database } from '@/api-lib/middlewares'; +import { getMongoDb } from '@/api-lib/mongodb'; import { UserPost } from '@/page-components/UserPost'; -import nc from 'next-connect'; import Head from 'next/head'; export default function UserPostPage({ post }) { @@ -21,9 +20,9 @@ export default function UserPostPage({ post }) { } export async function getServerSideProps(context) { - await nc().use(database).run(context.req, context.res); + const db = await getMongoDb(); - const post = await findPostById(context.req.db, context.params.postId); + const post = await findPostById(db, context.params.postId); if (!post) { return { notFound: true, diff --git a/pages/verify-email/[token].jsx b/pages/verify-email/[token].jsx index 313ea90..12ad43e 100644 --- a/pages/verify-email/[token].jsx +++ b/pages/verify-email/[token].jsx @@ -1,8 +1,6 @@ import { findAndDeleteTokenByIdAndType, updateUserById } from '@/api-lib/db'; -import { database } from '@/api-lib/middlewares'; -import { ncOpts } from '@/api-lib/nc'; +import { getMongoDb } from '@/api-lib/mongodb'; import { VerifyEmail } from '@/page-components/VerifyEmail'; -import nc from 'next-connect'; import Head from 'next/head'; export default function EmailVerifyPage({ valid }) { @@ -17,21 +15,19 @@ export default function EmailVerifyPage({ valid }) { } export async function getServerSideProps(context) { - const handler = nc(ncOpts); - handler.use(database); - await handler.run(context.req, context.res); + const db = await getMongoDb(); const { token } = context.params; const deletedToken = await findAndDeleteTokenByIdAndType( - context.req.db, + db, token, 'emailVerify' ); if (!deletedToken) return { props: { valid: false } }; - await updateUserById(context.req.db, deletedToken.creatorId, { + await updateUserById(db, deletedToken.creatorId, { emailVerified: true, });