This does not reflect the recent rewrite of nextjs-mongodb-app
. Check out the latest version.
Update: I have replaced argon2 with bcryptjs for compatibility.
For the last month, I have been struggling with implementing an authentication system for my next project. It is part of an organization that I found to tackle problems in the community with code.
The project is an online game that promotes good behaviors, so it needs to have a way for users to login.
I am using Next.js, a React framework, and MongoDB as my database.
I used to have an Express backend as a Next.js custom server, where I (lazily) use PassportJS to handle authentication. I realized that I did not much control of my APIs doing so. Therefore, I decided to go for my authentication system.
A possible counter-argument is that I should not reinvent the wheel. Still, I wanted exploration and challenges, so I took the path.
Most of the solutions I found online are either relied on 3rd party services (Google, Facebook, Identity as a Service) or half-baked without much thought into the real-life application. Thus, I decide to make my own.
Below are the Github repository and a demo for this project to follow along.
Demo (Fixed)
About nextjs-mongodb-app
project
nextjs-mongodb-app is a full-fledged app built with Next.JS and MongoDB. Most tutorials on the Internet are either half-baked or not production-ready. This project aims to fix that.
This project goes even further and attempts to integrate top features as seen in real-life apps, making it a full-fledged app.
For more information, visit the Github repo.
Getting started
Typical tutorials will tell you do to npm install next
or whatsoever, but I trust you to be an experienced developer already.
I will start by getting right into scaffolding my application.
Environmental variables
This project retrieves the variable from process.env
. You will need to implement your strategy. Some of the possible solutions are:
- https://github.com/zeit/next.js/tree/canary/examples/with-dotenv
- Setting the environment variables in your service provider (such as now.sh env, or heroku config var)
Required environmental variables in this project includes:
- process.env.MONGODB_URI
Request library
This project will need to have a request library to make requests to API. Feel free to pick your own. Here are some suggestions:
- axios (my choice)
- isomorphic-unfetch
- request
Validation library
I'm using validator for validation, but feel free to use your library or write your check.
Password hashing library
Password must be hashed. Period. There are different libraries out there:
And, no MD5, SHA1, or SHA256, please!
Mongoose... I mean MongoDB
Mongoose is not MongoDB. It is a whole different avenue. I see a lot of tutorials use Mongoose and MongoDB interchangeably, and that is so wrong.
Mongoose is an Object data modeling library. This ensures persistence and integrity of data by defining schemas in your database, which, in my opinion, destroys the purpose of NoSQL.
NoSQL is designed to drop validation and modeling (which is known as ACID). It means any type of data is accepted and future modification of the structure is flexible. On the other hand, Mongoose will require you to have a schema for everything. If I define to my Age
field to be Number
, trying to save a String
to the field will generate an Error.
Be aware that using Mongoose has a significant decrease in performance because data must be validated in every READ and WRITE (which justify dropping ACID
).
In case you are going to use Mongoose, here is the schema I have for User
:
const UserSchema = new mongoose.Schema({
email: {
type: String,
trim: true,
minlength: 1,
unique: true,
index: true,
},
emailVerified: {
type: Boolean,
},
password: {
type: String,
minlength: 6,
},
name: {
type: String,
},
});
export default mongoose.models.User || mongoose.model('User', UserSchema);
Building the full-fledged authentication.
Response schema
measure twice, cut once
I just cannot stress that enough. I would like to see a persistency in my APIs' responses. Thus, I come up with this schema - a standard response for every APIs:
{
"status": "ok / error",
"message": "a user-readable message",
"data": "<payload object>"
}
Feel free to have your own. For some inspirations, see this Stackoverflow discussion.
If I do not have this planned out, I may run into several problems. Imagine one of my endpoints responses with:
{
"success": true,
"msg": "successful stuff",
}
...while another one responses with:
{
"error": false,
"text": "ok",
}
I will have to write two different checks and tries to display the messages using two different field:
if (response.success === true || response.error === false) {
alert(response.msg || response.text);
}
It will get bad quick especially when the endpoints grow in number and get undocumented.
Middlewares
You may be familiar with the term middleware if you have an ExpressJS
background.
The way to use Middlewares in Next.js is to take the original handler
(API Route function of (req, res)
) an as argument and return a new handler
with additional functionality.
Database middleware
We will need to have a middleware that handles database connection since we do not want to call out the database in every route.
Creating middlewares/withDatabase.js
:
import { MongoClient } from 'mongodb';
const client = new MongoClient(process.env.MONGODB_URI, { useNewUrlParser: true });
const withDatabase = handler => (req, res) => {
if (!client.isConnected()) {
return client.connect().then(() => {
req.db = client.db('nextjsmongodbapp');
return handler(req, res);
});
}
req.db = client.db('nextjsmongodbapp');
return handler(req, res);
};
export default withDatabase;
What happens is that I first only attempt to connect if the client is not connected.
I then attach the database to req.db
and client to req.mongoClient
. The client can be reused later in our session middleware.
It is not recommended to hard-code your MongoDB URI or any other secure variable. I'm getting it via process.env
.
Session middleware
Session is one of the crux elements in our project. In ExpressJS, we can use express-session
. In Next.js, a session middleware to use is my next-session. You can go ahead and install it or use/make your middleware.
npm install next-session
Note: At the moment, next-session
does not have any native session store, but it is compatible with express-session
session stores by setting storePromisify
to true
.
The default store is MemoryStore
, which is not production-ready. For now, we can use connect-mongo since we are already using MongoDB:
Creating middlewares/withSession.js
:
import session from 'next-session';
import connectMongo from 'connect-mongo';
const MongoStore = connectMongo(session);
const withSession = handler => session.withSession(handler, {
store: new MongoStore({ url: process.env.MONGODB_URI }),
});
export default withSession;
MongoStore
also requires session
. Our session
middleware is next-session
.
Global middleware
This is my approach to middleware, I'm going to quote it from next-session
.
In reality, you would not want to wrap session() around handler in every function. You may run into a situation where the configuration of one session() is different from others. One solution is to create a global middleware.
Simply create middlewares/withMiddleware.js
and include our other middlewares:
import withDatabase from './withDatabase';
import withSession from './withSession';
const middleware = handler => withDatabase(withSession(handler));
export default middleware;
It is important to know that the order in withDatabase(withSession(handler))
is matter. Later we will add a middleware called withAuthentication
which make use of our database. Thus, we will have withAuthentication
inside withDatabase
and inside withSession
or the database will not be ready and the session will not exist then.
components/layout
: The Layout
This part is optional. This is where you can include your Header.jsx to have it on every page as long as you wrap the pages with <Layout>
.
I'm going to add some styles using Next.js styled-jsx
.
import React from 'react';
export default ({ children }) => (
<>
<style jsx global>
{`
* {
box-sizing: border-box;
}
body {
color: #4a4a4a;
background-color: #f8f8f8;
}
input {
width: 100%;
margin-top: 1rem;
padding: 1rem;
border: none;
background-color: rgba(0, 0, 0, 0.05);
}
button {
color: #ecf0f1;
margin-top: 1rem;
background: #009688;
border: none;
padding: 1rem;
}
`}
</style>
{ children }
</>
);
User registration
Let's start with the user registration since we need at least a user to work with.
Building the Signup API
Let's say we sign the user up by making a POST
request to /api/users
with a name, an email, and a password.
Everything in the /api
is an API Route, a new addition to Next.js 9. Each has to export a function that take two arguments req
and res
. Here is the content for our users.js
:
import isEmail from 'validator/lib/isEmail';
import * as argon2 from 'argon2';
import withMiddleware from '../../middlewares/withMiddleware';
const handler = (req, res) => {
if (req.method === 'POST') {
const { email, name, password } = req.body;
if (!isEmail(email)) {
return res.send({
status: 'error',
message: 'The email you entered is invalid.',
});
}
return req.db.collection('users').countDocuments({ email })
.then((count) => {
if (count) {
return Promise.reject(Error('The email has already been used.'));
}
return argon2.hash(password);
})
.then(hashedPassword => req.db.collection('users').insertOne({
email,
password: hashedPassword,
name,
}))
.then((user) => {
req.session.userId = user.insertedId;
res.status(201).send({
status: 'ok',
message: 'User signed up successfully',
});
})
.catch(error => res.send({
status: 'error',
message: error.toString(),
}));
}
return res.status(405).end();
};
export default withMiddleware(handler);
The function validates the email, hash the password, and insert a user into the database. After that, we set userId
in the session to the newly created object's id.
You can see that I first check that the method is POST
before proceeding. If it is not, I return status code 405 Method Not Allowed.
If the user is created, I set req.session.userId
to the created object id. I will explore it later.
Also note that in each case of error, I simply return a rejection with Error object. The rejection will be caught at .catch()
, where I send back a response with the error message via .toString()
(Error is an Error object, thus needed to be converted to a string).
pages/signup.jsx
: The sign up page
In signup.jsx
, we will have the following content:
import React, { useState } from 'react';
import axioswal from 'axioswal';
import Layout from '../components/layout';
import redirectTo from '../lib/redirectTo';
const SignupPage = () => {
const [name, setName] = useState('');
const [email, setEmail] = useState('');
const [password, setPassword] = useState('');
const handleSubmit = (event) => {
event.preventDefault();
axioswal
.post('/api/users', {
name,
email,
password,
})
.then((data) => {
if (data.status === 'ok') {
redirectTo('/');
}
});
};
return (
<Layout>
<div style={{ margin: '4rem' }}>
<h1>Sign up</h1>
<form onSubmit={handleSubmit}>
<div>
<input
type="text"
placeholder="Your name"
value={name}
onChange={e => setName(e.target.value)}
/>
</div>
<div>
<input
type="email"
placeholder="Email address"
value={email}
onChange={e => setEmail(e.target.value)}
/>
</div>
<div>
<input
type="password"
placeholder="Create a password"
value={password}
onChange={e => setPassword(e.target.value)}
/>
</div>
<button type="submit">
Sign up
</button>
</form>
</div>
</Layout>
);
};
export default SignupPage;
Awesome, let's start up our dev server, navigate to /signup
and check it out. Try sign up with a creative imaginary email and a badass password.
Also, try to:
- Sign up again with the same email.
- Entered some invalid email.
Does it show the error messages? If yes, great! Let's review what you have just done.
In case you are unfamiliar with Hook, I use the React State Hook useState
. In every textbox, I set its value to its corresponding state
variable (name
, email
, password
), and when it changes (onChange
), I call the the method (setName
, setEmail
, setPassword
) and apply its new value (via e.target.value
).
Looking at onSubmit={handleSubmit}
, we can see that the submission (either via a button or Enter) will call the function handleSubmit
, where we are going to add our login flow. event.preventDefault()
prevents the form to be submitted (to the current page, which causes the page to rerender).
What handleSubmit
should do beside preventDefault()
is to make a POST
request to /api/users
. After that, I also need to show an Error or Success message to the user.
I use my axioswal
, which make the Axios request, process the response, and show an sweetalert2 dialog based on the response.
npm install axioswal
If you are using a different request library or want to handle it on your own, feel free to do so.
Also, note that I redirect the user if he or she signs up successfully. I use a function defined at lib/redirectTo.js
:
import Router from 'next/router';
export default function redirectTo(destination, { res, status } = {}) {
if (res) {
res.writeHead(status || 302, { Location: destination });
res.end();
} else if (destination[0] === '/' && destination[1] !== '/') {
Router.push(destination);
} else {
window.location = destination;
}
}
Taken from a Github snippet, but I forgot where it was. If you know, tell me :(
User authentication
Now that we have one user. Let's try to authenticate the user. We actually did authenticate the user when he or she signs up:
req.session.userId = user.insertedId;
Let's see how we can do it in /login
, where we make a POST
request to /api/authenticate
.
Building the Authentication API
Let's create api/authenticate.js
:
import * as argon2 from 'argon2';
import withMiddleware from '../../middlewares/withMiddleware';
const handler = (req, res) => {
if (req.method === 'POST') {
const { email, password } = req.body;
return req.db.collection('users').findOne({ email })
.then((user) => {
if (user) {
return argon2.verify(user.password, password)
.then((result) => {
if (result) return Promise.resolve(user);
return Promise.reject(Error('The password you entered is incorrect'));
});
}
return Promise.reject(Error('The email does not exist'));
})
.then((user) => {
req.session.userId = user._id;
return res.send({
status: 'ok',
message: `Welcome back, ${user.name}!`,
});
})
.catch(error => res.send({
status: 'error',
message: error.toString(),
}));
}
return res.status(405).end();
};
export default withMiddleware(handler);
The logic is simple, we first look up the email by calling findOne
given the email
as our query. If the email does not exist we reject and say "The email does not exist". If the email does exist, req.db.collection.findOne returns a document which we refer to as user
.
We then try to match the passwords. I call argon2.verify
(which hash the received password and compare it with the hashed one) to see if the password
we get from the request matches the one in the database. If it does not match, argon2.verify()
returns false. We then reject and say "The password you entered is incorrect".
In reality, however, I may not want to have two distinct responses saying if it is the email or the password that is incorrect. It may allow brute-forcing. We can change it by simply changing each Error()
object to both say "Your email or password is incorrect".
If it matches, we set userId
to req.session
similar to Signup
, and return a message saying "Welcome back" with the name of the user.
pages/login.jsx
: The login page
Here is our code for pages/login.jsx
:
import React, { useState } from 'react';
import axioswal from 'axioswal';
import Layout from '../components/layout';
import redirectTo from '../lib/redirectTo';
const LoginPage = () => {
const [email, setEmail] = useState('');
const [password, setPassword] = useState('');
const handleSubmit = (event) => {
event.preventDefault();
axioswal
.post('/api/authenticate', {
email,
password,
})
.then((data) => {
if (data.status === 'ok') {
redirectTo('/');
}
});
};
return (
<Layout>
<div style={{ margin: '4rem' }}>
<h1>Log in</h1>
<form onSubmit={handleSubmit}>
<div>
<input
type="email"
placeholder="Email address"
value={email}
onChange={e => setEmail(e.target.value)}
/>
</div>
<div>
<input
type="password"
placeholder="Create a password"
value={password}
onChange={e => setPassword(e.target.value)}
/>
</div>
<button type="submit">
Log in
</button>
</form>
</div>
</Layout>
);
};
export default LoginPage;
If you look at it closely, I copy the whole thing from our signup.jsx
, remove the name
field, and change the POST
URL to api/authenticate
.
The logic is the same. We still redirect the user if he or she logs in successfully.
Start the server and navigate to /login
to see our result.
Did it work? If it did, awesome! We have managed to get through most of the project.
Using session to determine user identity
Recall that we set an userId in req.session
. What we can do is to have a middleware to look it up in our MongoDB database.
req.user
middleware
Go ahead and create middlewares/withAuthentication
:
import { ObjectId } from 'mongodb';
const withAuthentication = handler => (req, res) => {
if (req.session.userId) {
console.log(req.session.userId);
return req.db.collection('users').findOne(ObjectId(req.session.userId))
.then((user) => {
console.log(user);
if (user) req.user = user;
return handler(req, res);
});
}
return handler(req, res);
};
export default withAuthentication;
Include it in our withMiddleware.js
:
const middleware = handler => withDatabase(withSession(withAuthentication(handler)));
I mentioned that the order is matter because we need req.session
to be ready then.
If there is a req.session.userId
, we try to look the id up (making sure convert it into an ObjectId
first). If there is a user, we attach it to req
. Why? Because we may reuse this user
document in other endpoints. Since every endpoint goes through withAuthentication
, which available via withMiddleware
, we can determine the user by simply referring req.user
. This also helps us avoid repetitive codes.
Session endpoint to get current user
Let's have an endpoint that fetches the current user. I will have it /api/session
, but you can call it anything like /api/user
or /api/users/me
.
In /api/session
, put in the following content:
import withMiddleware from '../../middlewares/withMiddleware';
const handler = (req, res) => {
if (req.method === 'GET') {
if (req.user) {
const { name, email } = req.user;
return res.status(200).send({
status: 'ok',
data: {
isLoggedIn: true,
user: { name, email },
},
});
}
return res.status(200).send({
status: 'ok',
data: {
isLoggedIn: false,
user: {},
},
});
}
return res.status(405).end();
};
export default withMiddleware(handler);
In my opinion, it is appropriate to have the endpoint accepts GET
request for fetching current user information.
As you can see, I first see if a user is logged in simply by checking if req.user
exists.
If so, I responded saying isLoggedIn: true
and response with the user's name and email.
If not, I simply response saying isLoggedIn: false
and an empty user object.
User Context
We need to call GET /api/session
from somewhere and pass the data across React components. I manage to achieve it using React Context API.
Context provides a way to pass data through the component tree without having to pass props down manually at every level.
Yes, just what we need.
Another similar solution would be - I'm sure you heard this a lot - Redux. However, I believe Redux is overkill and the simpler the better.
Let's create components/UserContext.js
:
import React, { createContext, useReducer, useEffect } from 'react';
import axios from 'axios';
const UserContext = createContext();
const reducer = (state, action) => {
switch (action.type) {
case 'set':
return action.data;
case 'clear':
return {
isLoggedIn: false,
user: {},
};
default:
throw new Error();
}
};
const UserContextProvider = ({ children }) => {
const [state, dispatch] = useReducer(reducer, { isLoggedIn: false, user: {} });
const dispatchProxy = (action) => {
switch (action.type) {
case 'fetch':
return axios.get('/api/session')
.then(res => ({
isLoggedIn: res.data.data.isLoggedIn,
user: res.data.data.user,
}))
.then(({ isLoggedIn, user }) => {
dispatch({
type: 'set',
data: { isLoggedIn, user },
});
});
default:
return dispatch(action);
}
};
useEffect(() => {
dispatchProxy({ type: 'fetch' });
}, []);
return (
<UserContext.Provider value={{ state, dispatch: dispatchProxy }}>
{ children }
</UserContext.Provider>
);
};
const UserContextConsumer = UserContext.Consumer;
export { UserContext, UserContextProvider, UserContextConsumer };
Quite a lot going on here. I suggest reading about Context on React website and heading back here.
We first create a context by calling createContext()
.
I then create a Reducer (again, I would suggest do some reading first) using React Hook:
const [state, dispatch] = useReducer(reducer, { isLoggedIn: false, user: {} });
where { isLoggedIn: false, user: {} }
is the default value and reducer
is as defined above:
const reducer = (state, action) => {
switch (action.type) {
case 'set':
return action.data;
case 'clear':
return {
isLoggedIn: false,
user: {},
};
default:
throw new Error();
}
};
The value that is returned will be set to the state
.
For example, in the clear
function, I simply returned empty user object and isLoggedIn: false
.
Things get a little bit weird at set
.
Reducer does not support async function so I have to implement a little trick. I have a "proxy" function. All reducers will first go through dispatchProxy
.
If the reducer requires an async call such as fetch
dispatchProxy()
will do the asynchronous action and call dispatch()
when the action resolves. In particular, after fetching completed, it dispatchProxy()
calls dispatch()
and update the user context.
If the reducer does not contain an asynchronous call, I simply forward it to the actual dispatch()
.
Every time I need to fetch the user data (after logging in, or after changing profiles, etc.), I simply call the dispatch()
, which is available by simply importing the UserContext
. Also notice useEffect(() => {}, []);
: I want to fetch in the app first rendering.
Similar to the availability of reducer
, we can access the state
by importing UserContext
too. The state
will contains all the user information we need.
Provider
To use Context on child components, we need a ContextProvider at the higher-order components.
<Provider>
<Child1 />
<Child 2>
<Child 3 />
</Child 2>
</Provider>
Looking at the mapping above, Child 1
, Child 2
, Child 3
will be able to access the Context. The <Provider>
will have this format:
<MyContext.Provider value={/* some value */}>
If you look back to the return value of UserContextProvider
in UserContext.jsx
, you can see that we achieve that. Therefore, simply import UserContextProvider
in that high ordered component.
In Next.js, _app.js
is the highest component we have access to. Thus, we are going to have our UserContextProvider
imported there.
Creating pages/_app.jsx
:
import React from 'react';
import App, { Container } from 'next/app';
import { UserContextProvider } from '../components/UserContext';
class MyApp extends App {
render() {
const { Component, pageProps } = this.props;
return (
<Container>
<UserContextProvider>
<Component {...pageProps} />
</UserContextProvider>
</Container>
);
}
}
export default MyApp;
Accessing state
of UserContext
Open up our pages/index.js
and fill in the following:
import React, { useContext } from 'react';
import Link from 'next/link';
import { UserContext } from '../components/UserContext';
import Layout from '../components/layout';
const IndexPage = () => {
const { state: { isLoggedIn, user: { name } } } = useContext(UserContext);
return (
<Layout>
<div>
<h1>
Hello,
{' '}
{(isLoggedIn ? name : 'stranger.')}
</h1>
{(!isLoggedIn ? (
<>
<Link href="/login"><div><button>Login</button></div></Link>
<Link href="/signup"><div><button>Sign up</button></div></Link>
</>
) : <button>Logout</button>)}
</div>
</Layout>
);
};
export default IndexPage;
Sorry if const { state: { isLoggedIn, user: { name } } } = useContext(UserContext);
confuses you as I use some ES6 Syntax. I basically get isLoggedIn
and user.name
from the state
of UserContext
.
I also write a conditional rendering. If isLoggedIn === true
, I will render the name
. Otherwise, I will render "stranger"
. Also, below the Welcome text, if isLoggedIn === false
, I also render the two links to Login and Sign up. Similarly, we render the Logout
button if isLoggedIn === true
.
Dispatch fetch
in UserContext
Remember that we need to dispatch the fetch
reducer for everything to work.
Open pages/login.jsx
and pages/signup.jsx
and include the UserContext
. Here is the implementation for pages/login.jsx
. Try to do the other one on your own.
axioswal
.post('/api/authenticate', {
email,
password,
})
.then((data) => {
if (data.status === 'ok') {
// Fetch the user data for UserContext here
dispatch({ type: 'fetch' });
redirectTo('/');
}
});
dispatch({ type: 'fetch' });
will call the dispatchProxy()
, which make the GET
request and call dispatch()
to update the UserContext
.
Logout: delete userId
from session
Let's add functionality to the Logout button in index.jsx
:
import React, { useContext } from 'react';
import Link from 'next/link';
import axioswal from 'axioswal';
import { UserContext } from '../components/UserContext';
import Layout from '../components/layout';
const IndexPage = () => {
const { state: { isLoggedIn, user: { name } }, dispatch } = useContext(UserContext);
const handleLogout = (event) => {
event.preventDefault();
axioswal
.delete('/api/session')
.then((data) => {
if (data.status === 'ok') {
dispatch({ type: 'clear' });
}
});
};
return (
<Layout>
<div>
<h1>
Hello,
{' '}
{(isLoggedIn ? name : 'stranger.')}
</h1>
{(!isLoggedIn ? (
<>
<Link href="/login"><div><button>Login</button></div></Link>
<Link href="/signup"><div><button>Sign up</button></div></Link>
</>
) : <button onClick={handleLogout}>Logout</button>)}
</div>
</Layout>
);
};
export default IndexPage;
I import dispatch
from UserContext
. Also, I assign a function to the Logout button that makes a DELETE
request to /api/session
. If the request is a success, we call the clear
reducer to empty the UserContext
.
Obvious we need to add a DELETE request handler in api/session.js
:
import withMiddleware from '../../middlewares/withMiddleware';
const handler = (req, res) => {
if (req.method === 'GET') {
if (req.user) {
const { name, email } = req.user;
return res.status(200).send({
status: 'ok',
data: {
isLoggedIn: true,
user: { name, email },
},
});
}
return res.status(200).send({
status: 'ok',
data: {
isLoggedIn: false,
user: {},
},
});
}
if (req.method === 'DELETE') {
delete req.session.userId;
return res.status(200).send({
status: 'ok',
data: {
isLoggedIn: false,
message: 'You have been logged out.',
},
});
}
return res.status(405).end();
};
export default withMiddleware(handler);
Just like we set userId
to the session when we log in. All I have to do is to delete the userId
from the session to log out.
Conclusion
Alright, let's run our app and test it out. This will be the first step in building a full-fledged app using Next.js and MongoDB.
I hope this can be a boilerplate to launch your next great app. Again, check out the repository here. I do accept feature requests. What features should I add next time? Feel free to create an issue and tell me.
I'm not so good at writing, so if there is an issue with the article, please help me improve.
Good luck on your next Next.js + MongoDB project!
Top comments (8)
Hi Hoang. Thanks for this tutorial. I'm a bit confuse at first since I admit a beginner here and the tutorial seems not for the beginner but I did made it work somehow. However mine is not functioning at sign-in. It works at sign-up and I was able to add user with pics, etc.. But when I try to sign-in it just hang and didn't proceed. So I suspected it's the profile picture coming from cloudinary so I did add new user with no profile picture to see it will load but still not. I'm sure data is saved, I see it in my mongodb compass.
What did I missed here since I did not change source code other than my .env. thanks.
There was a bug in my res.redirect function. It should be fixed now
Hi, this is really awesome thing. I am really unable to find some auth tutorial for nextjs9. Some beginners like me can have problems with this. Good Luck :)
So usefull
Great tutorial, one note, 'isomorphic-unfetch' is the library recommened by NextJs, not 'isomorphic-fetch' (which appears to be an abandoned lib)
Thanks, I will update the information.
argon2 requires node-gyp, which is a nightmare to work with. If you’re having troubles, switch to different library (preferably scrypt)
If you are to use argon2, read its documentation on github (or node-gyp’s) on how to make node-gyp work. (For example, on windows, you need window-build-tools)
Very very helpful
Thanks