22
loading...
This website collects cookies to deliver better user experience
nextjs-mongodb-app is a Full-fledged serverless app made with Next.JS and MongoDB
serverless
Email Verification allows you to verify the emails users used to sign up by sending them a verification link.
Password Reset allows the users to reset their passwords from a reset link sent to their emails.
Password Change allows the users to change their passwords simply by inputting their old and new passwords.
import { useCurrentUser } from "@/lib/user";
import { useRouter } from "next/router";
import { useEffect, useCallback } from "react";
import { fetcher } from "@/lib/fetch";
const AboutYou = ({ user, mutate }) => {
/* ... */
};
const Auth = () => {
const oldPasswordRef = useRef();
const newPasswordRef = useRef();
const onSubmit = useCallback(async (e) => {
e.preventDefault();
try {
await fetcher("/api/user/password", {
method: "PUT",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({
oldPassword: oldPasswordRef.current.value,
newPassword: newPasswordRef.current.value,
}),
});
} catch (e) {
console.error(e.message);
} finally {
oldPasswordRef.current.value = "";
newPasswordRef.current.value = "";
}
}, []);
return (
<section>
<h4>Password</h4>
<form onSubmit={onSubmit}>
<input
type="password"
autoComplete="current-password"
ref={oldPasswordRef}
placeholder="Old Password"
/>
<input
type="password"
autoComplete="new-password"
ref={newPasswordRef}
placeholder="New Password"
/>
<button type="submit">Save</button>
</form>
</section>
);
};
const SettingsPage = () => {
const { data, error, mutate } = useCurrentUser();
const router = useRouter();
useEffect(() => {
if (!data && !error) return; // useCurrentUser might still be loading
if (!data.user) {
router.replace("/login");
}
}, [router, data, error]);
if (!data?.user) return null;
return (
<>
<AboutYou user={data.user} mutate={mutate} />
<Auth />
</>
);
};
export default SettingsPage;
/api/user/password
with the old and new password. After the request, we clear the new and old password fields./api/user/password
./pages/api/user/password/index.js
.import { auths, database, validateBody } from "@/api-lib/middlewares";
import nc from "next-connect";
const handler = nc();
handler.use(database, ...auths);
handler.put(
validateBody({
type: "object",
properties: {
oldPassword: { type: "string", minLength: 8 },
newPassword: { type: "string", minLength: 8 },
},
required: ["oldPassword", "newPassword"],
additionalProperties: false,
}),
async (req, res) => {
if (!req.user) {
res.json(401).end();
return;
}
const { oldPassword, newPassword } = req.body;
// We could not req.user because that object does not have the `password` field
const currentUser = await db
.collection("users")
.findOne({ _id: req.user._id });
const matched = await bcrypt.compare(oldPassword, currentUser.password);
if (!matched) {
res.status(401).json({
error: { message: "The old password you entered is incorrect." },
});
return;
}
const password = await bcrypt.hash(newPassword, 10);
await req.db
.collection("users")
.updateOne({ _id: currentUser._id }, { $set: { password } });
res.status(204).end();
}
);
export default handler;
req.user
. If not, it will send a 401 response.req.user.password
. However, the new version uses projection
to omit that field when authenticating users for security reasons.)oldPassword
and newPassword
from the request body. The oldPassword
is compared against the hashed current password (bcrypt.compare(oldPassword, currentUser.password)
). If it does not match, we reject the request. If it does we hash the new password (bcrypt.hash(newPassword, 10)
) and save it in our database.await req.db
.collection("users")
.updateOne({ _id: req.user._id }, { $set: { password } });
POST /pages/api/user/password/reset
: Handle requests to create a password reset token and send emailPUT /pages/api/user/password/reset
: Reset password using a token./pages/api/user/password/reset/index.js
:import { sendMail } from "@/api-lib/mail";
import { database, validateBody } from "@/api-lib/middlewares";
import nc from "next-connect";
import normalizeEmail from "validator/lib/normalizeEmail";
import { nanoid } from "nanoid";
const handler = nc();
handler.use(database);
handler.post(
validateBody({
type: "object",
properties: {
email: { type: "string", minLength: 1 },
},
required: ["email"],
additionalProperties: false,
}),
async (req, res) => {
const email = normalizeEmail(req.body.email);
const user = await req.db.collection("users").findOne({ email });
if (!user) {
res.status(400).json({
error: { message: "We couldn’t find that email. Please try again." },
});
return;
}
const securedTokenId = nanoid(32); // create a secure reset password token
await db.collection("tokens").insertOne({
_id: securedTokenId,
creatorId: user._id,
type: "passwordReset",
expireAt: new Date(Date.now() + 20 * 60 * 1000), // let's make it expire after 20 min
});
await sendMail({
to: user.email,
from: "[email protected]",
subject: "[nextjs-mongodb-app] Reset your password.",
html: `
<div>
<p>Hello, ${user.name}</p>
<p>Please follow <a href="${process.env.WEB_URI}/forget-password/${securedTokenId}">this link</a> to reset your password.</p>
</div>
`,
});
res.status(204).end();
}
);
export default handler;
req.db.collection('users').findOne({ email: req.body.email })
.passwordReset
token. We insert a document to our tokens
collection with the created token along with the intended user's _id in creatorId
. The secure token is set as the document _id
(since _id
is indexed by MongoDB, it allows faster lookup)expireAt
property for security reasons, we can create an index on that collection like below (usually called when the server starts):db.collection("tokens").createIndex("expireAt", { expireAfterSeconds: 0 });
website_url/forget-password/{token}
)./pages/forget-password/index.jsx
import { fetcher } from "@/lib/fetch";
import { useCallback, useRef } from "react";
const ForgetPasswordPage = () => {
const emailRef = useRef();
const onSubmit = useCallback(async (e) => {
e.preventDefault();
try {
await fetcher("/api/user/password/reset", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({
email: emailRef.current.value,
}),
});
} catch (e) {
console.error(e.message);
}
}, []);
return (
<>
<Head>
<title>Forget password</title>
</Head>
<h1>Forget password</h1>
<p>
Enter the email address associated with your account, and we'll
send you a link to reset your password.
</p>
<form onSubmit={onSubmit}>
<input
ref={emailRef}
type="email"
autoComplete="email"
placeholder="Email"
/>
<button type="submit">Submit</button>
</form>
</>
);
};
export default ForgetPasswordPage;
/pages/api/user/password/reset.js
:import nc from "next-connect";
import bcrypt from "bcryptjs";
import database from "@/api-lib/middlewares";
const handler = nc();
handler.use(database);
handler.post(/* ... */);
handler.put(
validateBody({
type: "object",
properties: {
password: { type: "string", minLength: 8 },
token: { type: "string", minLength: 0 },
},
required: ["password", "token"],
additionalProperties: false,
}),
async (req, res) => {
const deletedToken = await db
.collection("tokens")
.findOneAndDelete({ _id: id, type });
if (!deletedToken) {
res.status(403).end();
return;
}
const password = await bcrypt.hash(newPassword, 10);
await db
.collection("users")
.updateOne(
{ _id: deletedToken.creatorId },
{ $set: { password } }
);
res.status(204).end();
}
);
export default handler;
deleteToken
is not null, we also know that the token is deleted from the database (we don't want the same token to be used twice).bcrypt
and update the user whose _id
can be found at token.creatorId
.website_url/forget-password/{token}
). Create /pages/forget-password/[token].jsx
:import { database } from "@/api-lib/middlewares";
import nc from "next-connect";
import Head from "next/head";
const ResetPasswordTokenPage = ({ valid, token }) => {
const passwordRef = useRef();
const onSubmit = useCallback(
async (event) => {
event.preventDefault();
setStatus("loading");
try {
await fetcher("/api/user/password/reset", {
method: "PUT",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({
token,
password: passwordRef.current.value,
}),
});
} catch (e) {
console.error(e.message);
}
},
[token]
);
if (!valid)
return (
<>
<h1>Invalid Link</h1>
<p>
It looks like you may have clicked on an invalid link. Please close
this window and try again.
</p>
</>
);
return (
<>
<Head>
<title>Forget password</title>
</Head>
<h1>Forget password</h1>
<p>Enter a new password for your account</p>
<form onSubmit={onSubmit}>
<input
ref={passwordRef}
type="password"
autoComplete="new-password"
placeholder="New Password"
/>
<button type="submit">Reset password</button>
</form>
</>
);
};
export async function getServerSideProps(context) {
await nc().use(database).run(context.req, context.res);
const tokenDoc = await db.collection("tokens").findOne({
_id: context.params.token,
type: "passwordReset",
});
return { props: { token: context.params.token, valid: !!tokenDoc } };
}
export default ResetPasswordTokenPage;
token
can be found in context.params
since our page is a dynamic route (with dynamic token
parameter: /pages/forget-password/[token].jsx
).database
middleware is used to load the database into req.db
. We use it to check if the token can be found in the database by querying its _id
(we set the secure token as the document _id
earlier).valid
prop, which is true
if the token document can be found and false
otherwise.valid
is false
, we show a "Invalid Link" message.await fetcher("/api/user/password/reset", {
method: "PUT",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({
token,
password: passwordRef.current.value,
}),
});
/pages/api/user/email/verify.js
. This will handle users' requests to receive a confirmation email.import { sendMail } from "@/api-lib/mail";
import { auths, database } from "@/api-lib/middlewares";
import nc from "next-connect";
const handler = nc();
handler.post(async (req, res) => {
if (!req.user) {
res.json(401).end();
return;
}
const securedTokenId = nanoid(32);
const token = await {
_id: securedTokenId,
creatorId: req.user._id,
type: "emailVerify",
expireAt: new Date(Date.now() + 1000 * 60 * 60 * 24), // expires in 24h
};
await sendMail({
to: req.user.email,
from: "[email protected]",
subject: `Verification Email for ${process.env.WEB_URI}`,
html: `
<div>
<p>Hello, ${req.user.name}</p>
<p>Please follow <a href="${process.env.WEB_URI}/verify-email/${token._id}">this link</a> to confirm your email.</p>
</div>
`,
});
res.status(204).end();
});
export default handler;
req.user._id
. This token will expire in 24 hours./pages/verify-email/[token].jsx
.import { database } from "@/api-lib/middlewares";
import nc from "next-connect";
import Head from "next/head";
export default function EmailVerifyPage({ valid }) {
return (
<>
<Head>
<title>Email verification</title>
</Head>
<p>
{valid
? "Thank you for verifying your email address. You may close this page."
: "It looks like you may have clicked on an invalid link. Please close this window and try again."}
</p>
</>
);
}
export async function getServerSideProps(context) {
const handler = nc(ncOpts);
handler.use(database);
await handler.run(context.req, context.res);
const { token } = context.params;
const deletedToken = await db
.collection("tokens")
.findOneAndDelete({ _id: token, type: "emailVerify" });
if (!deletedToken) return { props: { valid: false } };
await db.collection("users").updateOne(
{ _id: deletedToken.creatorId },
{
emailVerified: true,
}
);
return { props: { valid: true } };
}
getServerSideProps
. Similar to the reset password feature, we find and delete the token from the database. If the token is found (and thus deleted), we update the user whose _id
found in the token creatorId
to have emailVerify = true
.valid
inform the UI to show the correct message./api/user
actually contains a property call emailVerified
, as seen above.useCurrentUser
SWR hook to show a message asking the user to verify his or her email.export default () => {
const {
data: { user },
} = useCurrentUser();
if (!user.emailVerified)
return (
<p>
<strong>Note:</strong> <span>Your email</span> (<span className="link">
{user.email}
</span>) is unverified.
</p>
);
return null;
};