How I built my portfolio using Next.JS and SQLite DB — [Part 3]

Krishna Prasad
7 min readApr 24, 2024

Authentication system with a hand-coded simple auth middleware

Next.JS, SQLite, Typescript project

Hello again fellow devs! Welcome to Part 3 of building your portfolio using Next.JS and SQLite DB! In this part, let us discuss about the authentication system, JWT token, route protection of our portfolio app’s API structure.

We will be diving deeper into —

  1. Building a login and signup APIs.
  2. Encrypting password using bcrypt package and using JWT token for our auth session.
  3. Writing a middleware HOC (Higher Order Component) to check the auth status for protected routes.

So far, we have a very basic installation of out Next.JS app that just serves the standard Next.JS starter page and an admin/login page for dashboard authentication, API implementation basics and SQLite DB connections. If you haven’t already read through it, I recommend checking it out here —

The idea of this part of the series is that we want to update our home page content and create articles to be reflected on our portfolio from a dashboard that only we have access to. The routes to POST, PATCH and DELETE are going to be protected routes where you will need a JWT token to authenticate the db transaction. To achieve this, we will write a middleware HOC that will verify the received JWT token before allowing the protected operation. A quick note here

For this tutorial, I will be writing a very simple auth login check manually so that we have a clear idea of what we are trying to achieve. If you are looking at a larger scale of application / security, please look into using packages like next-auth or next-iron-session that provide a middleware with other features included.

1. Installing dependencies

Let us see how we can achieve this. First off, let us add the dependencies — bcrypt and jsonwebtoken.

yarn add bcrypt jsonwebtoken

Once the dependencies are installed, we need to add to our migrations.ts to create a users table that will host an id, name, emailId, password fields and then run the migration by your preferred choice.

// /src/app/api/migrations.ts

import { db } from "./database";

export const runMigratins = () => {
db.serialize(() => {
// we created articles table here. You can paste the
// below code right after and run it.
db.run(
`
CREATE TABLE IF NOT EXISTS users (
id INTEGER PRIMARY KEY AUTOINCREMENT,
name TEXT NOT NULL,
emailId TEXT UNIQUE NOT NULL,
password TEXT NOT NULL
);
`,
(err: Error) => {
if (err) {
console.error(err.message);
}
console.log("users table created successfully.");
}
);
});
}

On running this, you should get a users table created successfully log on your console. Now that we have created the users table, we need to add a user to the table before we can write our authentication code.

2. Creating /auth/signup route

Let’s create a signup route that hosts the signup code. I have chosen the route to be /api/auth/signup. To do this, let us create a folder inside api and call it auth, create a folder inside that called signup and place a route.tsx file in it to serve as our route file. Signup is going to be a POST API with a body containing the user’s information that we filled in the users table above. Let us look at the signup function —

// /app/api/auth/signup/route.ts

import { apiPost } from "../../database";
import { withAuth } from "../../middlewares/withAuth";

const bcrypt = require("bcrypt");

export async function POST(req: Request, res: Response) {
const body = await req.json();
const { name, emailId, password } = body;

const saltRounds = 10;
const hashedPassword = await bcrypt.hash(password, saltRounds);

const query = `
INSERT INTO users(name, emailId, password)
VALUES(?, ?, ?)
`;
const values = [name, emailId, hashedPassword];

let status, respBody;
await apiPost(query, values)
.then(() => {
status = 200;
respBody = { message: "Successfully created user" };
})
.catch((err) => {
status = 400;
respBody = err;
});
return Response.json(respBody, {
status,
});
}

Let us import bcrypt to encrypt our password field since storing passwords as plain text can cause security issues. Apart from the hashedPassword that is a bcrypt encrypted password, the rest is plain simple I presume and so we can now hit http://localhost:3000/api/auth/signup would create a user in your profile.db.

3. Creating /auth/login route

Now that we have created a user, we can look into creating our login logic. For this, we need to also first consider getting the user record by provided emailId. We don’t need to expose this as an API route, so we can add a users/route.ts file and just export a named function like so —

// /app/api/users/route.ts

import { User } from "@/app/interfaces/user";
import { apiGet } from "../database";

export const getUserByEmailId = async (emailId: string) => {
return await new Promise(async (resolve, reject) => {
await apiGet(`SELECT * from users WHERE emailId = ?`, [emailId])
.then((res) => {
const user = (res as User[])[0];
resolve(user)
})
.catch((err: Error) => {
reject(undefined)
});
});
};

Like always, since the API get using SQLite drivers works asynchronously, we need to manually wrap it in a promise so that we can await it properly on the other end.

Moving on to creating the actual login function, let us create a route /app/api/auth/login/route.ts and add the following code into it.

// /app/api/auth/login/route.ts

import { User } from "@/app/interfaces/user";
import { getUserByEmailId } from "../../users/route";

const jwt = require("jsonwebtoken");
const bcrypt = require("bcrypt");

export async function POST(req: Request, res: Response) {
const body = await req.json();
const { emailId, password } = body;

let respBody, status;
await getUserByEmailId(emailId)
.then((res) => {
const user = res as User;
if (user && bcrypt.compare(password, user?.password)) {
const token = jwt.sign({ id: user.id }, process.env.JWT_SECRET);
respBody = { token };
status = 200;
} else {
respBody = { message: "Check email and password" };
status = 401;
}
})
.catch((err: Error) => {
respBody = { message: "Check email and password" };
status = 401;
});
return Response.json(respBody, {
status,
});
}

Same as the one we wrote for our articles route, key takeaways being —

  1. Fetch the user table data with the provided emailId.
  2. Use bcrypt.compare to compare hashed password (from the users response) with the plain password that that user has sent via formData.
  3. Sign a JWT token with {id: user.id} information in it and pass it as a token in the response.

Now you can make a login request with the emailId and password you set in your signup route to login to the system (as in get the token necessary). Although, you can still make the articles request at this point even without the token and it works! We need to stop this. Let us consider writing the withAuth HOC middleware that validates the token before alloing the operation to be performed.

4. Creating the withAuth HOC

Create a route /api/middlewares/withAuth.ts. Since this is not a route and will not be accessible out of the system, you are free to name it up to your requirement! Let us now verify the token —

// /api/middlewares/withAuth.ts

import { headers } from "next/headers";

const jwt = require("jsonwebtoken");

export async function withAuth(request: Request, handler: Function) {
const unauthorizedResponse = Response.json({message: "Resource unauthorized"}, {status: 401});
const headersList = headers()
const authorization = headersList.get('Authorization')?.split(' ')[1];
if (!authorization) {
return unauthorizedResponse;
}

try {
const isAuthenticated = jwt.verify(authorization, process.env.JWT_SECRET);
if (!isAuthenticated) {
return unauthorizedResponse;
}

return handler(request);
} catch (error) {
return unauthorizedResponse;
}
}

3 takeaway scenarios that we need to handle in this route are —

  1. There is no JWT token sent at all in the header.
  2. jwt.verify() failed.
  3. The value of jwt.verify() is null / empty.

In all the above cases, the response would be the same 401 unauthorized. If it passes all three requirements, we can return the callback handler and pass the request back.

Now that the middleware is available, let us use it in our articles POST method since we want to disallow anybody other than ourselves to create an article record. Let us go back to the articles route.ts file and modify the POST call like so —

// /api/articles/route.ts

export async function POST(req: Request, res: Response) {
return withAuth(req, async (request: Request) => {
const body = await req.json();
const { name, description, imageUrl, articleUrl, slug } = body;

const query = `
INSERT INTO articles(name, description, imageUrl, articleUrl, slug)
VALUES(?, ?, ?, ?, ?)
`;
const values = [name, description, imageUrl, articleUrl, slug];

let status, respBody;
await apiPost(query, values)
.then(() => {
status = 200;
respBody = { message: "Successfully created article" };
})
.catch((err) => {
status = 400;
respBody = err;
});
return Response.json(respBody, {
status,
});
});
}

And boom! try making the POST request without your token and it will return a 401 unauthorized error!

With that, we have a fully functional auth APIs and our protected routes setup. I will stop here for this part. Please feel free to play around with authentication packages like next-auth. We will meet again in Part 4 of this journey where we can create a dashboard to manage our articles data.

Until then fellow devs! May the code be with you ✋ peace out and CHEERS!!

--

--

Krishna Prasad

ReactJS, NextJS, NodeJS and every other JS! It’s like a never ending learning journey. 😎