66-Nodejs Course 2023: Auth: Current User
Hasan Zohdy
Posted on November 17, 2022
In our previous article, we'have decoupled our JWT handler, now we need to get access to current user when the token is verified, stay focused
in this chapter as things are going to be big, BIIIIIIIG
How this works
Here is how it works, first we need to define a Road Map
to all of user types in the project, for example if the project has Admin
, Customer
and Vendor
, so we've here 4 user types, All the previous ones beside the Guest
user type.
We'll define a map that contains all the user types and their corresponding Model
To do so, we need to create an auth.ts
config file in our src/config
directory.
import User from "app/users/models/user";
import Guest from "core/auth/models/guest";
const authConfigurations = {
userTypes: {
guest: Guest,
user: User,
},
};
export default authConfigurations;
The next thing we need to do is to create the Guest
model, for now we'll make it inside the auth/models
directory as this one won't have much to do inside it.
Let's create it quickly.
// src/core/auth/models/guest.ts
import { Model } from "core/database";
export default class Guest extends Model {
/**
* {@inheritDoc}
*/
public static collectionName = "guests";
}
Now we defined our user types list, but don't forget to import it in our config list.
Open src/config/index.ts
and add the following line:
// src/config/index.ts
import config from "@mongez/config";
import databaseConfigurations from "config/database";
import appConfigurations from "./app";
import authConfigurations from "./auth";
import validationConfigurations from "./validation";
config.set({
database: databaseConfigurations,
validation: validationConfigurations,
app: appConfigurations,
auth: authConfigurations,
});
So we imported the auth configurations in our base configurations so we can use it from the auth directory.
Now we imported the configurations but it has no use as we didn't use it in our core code, but before we use it we need to know first what is the purpose of defining these user types.
The purpose
The purpose of defining these user types is when we verify the incoming access token from the Authorization
header we're already have the userType
as we are adding it (See the guest route), so when we verify the token we need to define the current user by getting the user/guest/admin/customer from the database using its defined model in the configuration and mark it as current User
.
Creating New Guest
We created the Guest
model, but we didn't use it yet, so here it how we'll use it, we're going to create a new document for that guest when the /guest
route is called and before generating the token, we'll first create a new guest document and then generate the token, that token will hold the userType
and the _id
of the guest document.
A note about _id
and id
and when we use either one
You might get confused why i'm using _id
most of the time and we already did create id
with much effort? well because of two things, the first one the _id
will always be generated so i can count on it on the low level code, which is in core
directory, and the second one is that the _id
field is indexed by default, so it's faster to search for it. (We didn't come to indexes yet in MongoDB 😔)
Generating new guest
Now let's go to registerAuthRoutes
file and import our new model Guest
to generate a new guest document.
// src/core/auth/registerAuthRoutes.ts
import Guest from "./models/guest";
//...
export default function registerAuthRoutes() {
//...
// now let's add a guests route in our routes to generate a guest token to our guests.
router.post("/guests", async (_request, response: Response) => {
// generate a new guest first
const guest = await Guest.create({
userType: "guest",
});
// use our own jwt generator to generate a token for the guest
const token = await jwt.generate(guest.data);
AccessToken.create({
token,
// get the guest user type, id and _id
...guest.only(["id", "userType", "_id"]),
});
return response.send({
accessToken: token,
// return the user type
userType: guest.get("userType"),
});
});
// ...
}
We created first a new document of guest to generate its ids, then we generated a token using that guest data, then we saved the token in the database, and finally we returned the token and the user type.
So the token now has its own payload, which is the userType
and the _id
of the guest document.
Finding By _id
We're missing something here, MongoDB requires the _id
to be an object, so we need to convert it to an object before we use it to find the document.
So we need to update our crud-model
first, then we need ta create a method that to find a document by its _id
.
// src/core/database/crud-model.ts
// ...
/**
* Find document by the given column and value
*/
public static async findBy<T>(
this: ChildModel<T>,
column: string,
value: any,
): Promise<T | null> {
const query = this.query();
// if column is _id and value is string, convert it to ObjectId
if (column === "_id" && typeof value === "string") {
value = new ObjectId(value);
}
const result = await query.findOne({
[column]: value,
});
return result ? this.self(result as ModelDocument) : null;
}
/**
* List multiple documents based on the given filter
*/
public static async list<T>(
this: ChildModel<T>,
filter: Filter = {},
): Promise<T[]> {
// if filter contains _id and it is a string, convert it to ObjectId
if (filter._id && typeof filter._id === "string") {
filter._id = new ObjectId(filter._id);
}
const documents = await queryBuilder.list(this.collectionName, filter);
return documents.map(document => this.self(document));
}
/**
* Get first model for the given filter
*/
public static async first<T>(
this: ChildModel<T>,
filter: Filter = {},
): Promise<T | null> {
// if filter contains _id and it is a string, convert it to ObjectId
if (filter._id && typeof filter._id === "string") {
filter._id = new ObjectId(filter._id);
}
const result = await queryBuilder.first(this.collectionName, filter);
return result ? this.self(result) : null;
}
/**
* Get last model for the given filter
*/
public static async last<T>(
this: ChildModel<T>,
filter: Filter = {},
): Promise<T | null> {
// if filter contains _id and it is a string, convert it to ObjectId
if (filter._id && typeof filter._id === "string") {
filter._id = new ObjectId(filter._id);
}
const result = await queryBuilder.last(this.collectionName, filter);
return result ? this.self(result) : null;
}
/**
* Get latest documents
*/
public static async latest<T>(
this: ChildModel<T>,
filter: Filter = {},
): Promise<T[]> {
// if filter contains _id and it is a string, convert it to ObjectId
if (filter._id && typeof filter._id === "string") {
filter._id = new ObjectId(filter._id);
}
const documents = await queryBuilder.latest(this.collectionName, filter);
return documents.map(document => this.self(document));
}
/**
* Delete single document if the given filter is an ObjectId of mongodb
* Otherwise, delete multiple documents based on the given filter object
*/
public static async delete<T>(
this: ChildModel<T>,
filter: PrimaryIdType | Filter,
): Promise<number> {
if (
filter instanceof ObjectId ||
typeof filter === "string" ||
typeof filter === "number"
) {
// if filter is a string and primary id column is _id, convert it to ObjectId
if (typeof filter === "string" && this.primaryIdColumn === "_id") {
filter = new ObjectId(filter);
}
return (await queryBuilder.deleteOne(this.collectionName, {
[this.primaryIdColumn]: filter,
}))
? 1
: 0;
}
return await queryBuilder.delete(this.collectionName, filter);
}
We added a check here in most of the methods to check if filter contains _id
and it's a string, then we convert it to ObjectId
.
But let's enhance it as we're duplicating the code multiple times, let's make a method to make that check.
// src/core/database/crud-model.ts
// ...
/**
* Prepare filters
*/
protected static prepareFilters(filters: Filter = {}) {
// if filter contains _id and it is a string, convert it to ObjectId
if (filters._id && typeof filters._id === "string") {
filters._id = new ObjectId(filters._id);
}
return filters;
}
Now let's update all methods to use that filter method preparing.
The entire file will look like:
// src/core/database/crud-model.ts
import { ObjectId } from "mongodb";
import queryBuilder from "../query-builder/query-builder";
import BaseModel from "./base-model";
import {
ChildModel,
Document,
Filter,
ModelDocument,
PaginationListing,
PrimaryIdType,
} from "./types";
export default abstract class CrudModel extends BaseModel {
/**
* Create a new record in the database for the current model (child class of this one)
* and return a new instance of it with the created data and the new generated id
*/
public static async create<T>(
this: ChildModel<T>,
data: Document,
): Promise<T> {
const model = this.self(data); // get new instance of model
// save the model, and generate the proper columns needed
await model.save();
return model;
}
/**
* Update model by the given id
*/
public static async update<T>(
this: ChildModel<T>,
id: PrimaryIdType,
data: Document,
): Promise<T | null> {
const model = (await this.find(id)) as any;
// execute the update operation
if (!model) return null;
await model.save(data);
return model;
}
/**
* Replace the entire document for the given document id with the given new data
*/
public static async replace<T>(
this: ChildModel<T>,
id: PrimaryIdType,
data: Document,
): Promise<T | null> {
const model = (await this.find(id)) as any;
if (!model) return null;
model.replaceWith(data);
await model.save();
return model;
}
/**
* Restore the document from trash
*/
public static async restore<T>(
this: ChildModel<T>,
id: PrimaryIdType,
): Promise<T | null> {
// retrieve the document from trash collection
const result = await queryBuilder.first(
this.collectionName + "Trash",
this.prepareFilters({
[this.primaryIdColumn]: id,
}),
);
if (!result) return null;
const document = result.document;
// otherwise, create a new model with it
document.restoredAt = new Date();
const model = this.self(document);
model.markAsRestored();
await model.save(); // save again in the same collection
return model;
}
/**
* Find and update the document for the given filter with the given data or create a new document/record
* if filter has no matching
*/
public static async upsert<T>(
this: ChildModel<T>,
filter: Filter,
data: Document,
): Promise<T> {
filter = this.prepareFilters(filter);
let model = (await this.first(filter)) as any;
if (!model) {
model = this.self({ ...data, ...filter });
} else {
model.merge(data);
}
await model.save();
return model;
}
/**
* Find document by id
*/
public static async find<T>(this: ChildModel<T>, id: PrimaryIdType) {
return this.findBy(this.primaryIdColumn, id);
}
/**
* Find document by the given column and value
*/
public static async findBy<T>(
this: ChildModel<T>,
column: string,
value: any,
): Promise<T | null> {
const result = await queryBuilder.first(
this.collectionName,
this.prepareFilters({
[column]: value,
}),
);
return result ? this.self(result as ModelDocument) : null;
}
/**
* List multiple documents based on the given filter
*/
public static async list<T>(
this: ChildModel<T>,
filter: Filter = {},
): Promise<T[]> {
const documents = await queryBuilder.list(
this.collectionName,
this.prepareFilters(filter),
);
return documents.map(document => this.self(document));
}
/**
* Paginate records based on the given filter
*/
public static async paginate<T>(
this: ChildModel<T>,
filter: Filter,
page: number,
limit: number,
): Promise<PaginationListing<T>> {
filter = this.prepareFilters(filter);
const documents = await queryBuilder.list(
this.collectionName,
filter,
query => {
query.skip((page - 1) * limit).limit(limit);
},
);
const totalDocumentsOfFilter = await queryBuilder.count(
this.collectionName,
filter,
);
const result: PaginationListing<T> = {
documents: documents.map(document => this.self(document)),
paginationInfo: {
limit,
page,
result: documents.length,
total: totalDocumentsOfFilter,
pages: Math.ceil(totalDocumentsOfFilter / limit),
},
};
return result;
}
/**
* Count total documents based on the given filter
*/
public static async count(filter: Filter = {}) {
return await queryBuilder.count(
this.collectionName,
this.prepareFilters(filter),
);
}
/**
* Get first model for the given filter
*/
public static async first<T>(
this: ChildModel<T>,
filter: Filter = {},
): Promise<T | null> {
const result = await queryBuilder.first(
this.collectionName,
this.prepareFilters(filter),
);
return result ? this.self(result) : null;
}
/**
* Get last model for the given filter
*/
public static async last<T>(
this: ChildModel<T>,
filter: Filter = {},
): Promise<T | null> {
const result = await queryBuilder.last(
this.collectionName,
this.prepareFilters(filter),
);
return result ? this.self(result) : null;
}
/**
* Get latest documents
*/
public static async latest<T>(
this: ChildModel<T>,
filter: Filter = {},
): Promise<T[]> {
const documents = await queryBuilder.latest(
this.collectionName,
this.prepareFilters(filter),
);
return documents.map(document => this.self(document));
}
/**
* Delete single document if the given filter is an ObjectId of mongodb
* Otherwise, delete multiple documents based on the given filter object
*/
public static async delete<T>(
this: ChildModel<T>,
filter: PrimaryIdType | Filter,
): Promise<number> {
if (
filter instanceof ObjectId ||
typeof filter === "string" ||
typeof filter === "number"
) {
return (await queryBuilder.deleteOne(
this.collectionName,
this.prepareFilters({
[this.primaryIdColumn]: filter,
}),
))
? 1
: 0;
}
return await queryBuilder.delete(this.collectionName, filter);
}
/**
* Prepare filters
*/
protected static prepareFilters(filters: Filter = {}) {
// if filter contains _id and it is a string, convert it to ObjectId
if (filters._id && typeof filters._id === "string") {
filters._id = new ObjectId(filters._id);
}
return filters;
}
}
Now we're ready to continue, let's move on.
Getting the current user
Now once the token is verified using jwt.verify
, we can get the data from the request
object from Fastify's request object.
After getting the payload from request.user
property, we can get two values from it _id
and userType
, we'll use the userType
value to get its corresponding model class to get an instance of that model, then we'll use the _id
to get the document from the database.
Auth Middleware
Before we update the token verification, let's move the code to a middleware, so this can be customized easily.
We will remove the onRequest
event as we really don't do it, we just did it to make sure it works.
// src/core/auth/registerAuthRoutes.ts
import { Response } from "core/http/response";
import router from "core/router";
import jwt from "./jwt";
import AccessToken from "./models/access-token";
import Guest from "./models/guest";
export default function registerAuthRoutes() {
// now let's add a guests route in our routes to generate a guest token to our guests.
router.post("/guests", async (_request, response: Response) => {
// generate a new guest first
const guest = await Guest.create({
userType: "guest",
});
// use our own jwt generator to generate a token for the guest
const token = await jwt.generate(guest.data);
AccessToken.create({
token,
// get the guest user type, id and _id
...guest.only(["id", "userType", "_id"]),
});
return response.send({
accessToken: token,
// return the user type
userType: guest.get("userType"),
});
});
}
Now let's create auth-middleware
file and create our own middleware.
// src/core/auth/auth-middleware.ts
import config from "@mongez/config";
import { Request } from "core/http/request";
import { Response } from "core/http/response";
import jwt from "./jwt";
export async function authMiddleware(request: Request, response: Response) {
try {
// use our own jwt verify to verify the token
await jwt.verify();
// get current user
const user: any = request.baseRequest.user;
// now, we need to get an instance of user using its corresponding model
const userType = user.userType;
// get user model class
const UserModel = config.get(`auth.userType.${userType}`);
// get user model instance
const currentUser = await UserModel.findBy("_id", user._id);
// log user data
console.log(currentUser);
} catch (err) {
return response.badRequest({
error: "Unauthorized: Invalid Access Token",
});
}
}
You can now know why we made that update, the _id
is received as a string, so it won't be found in the collection, that's why we needed to convert it to an ObjectId
object.
Registering the middleware
Now let's register the middleware to listUsers
route.
// src/app/users/routes.ts
import { authMiddleware } from "core/auth/auth-middleware";
import router from "core/router";
import login from "./controllers/auth/login";
import createUser from "./controllers/create-user";
import getUser from "./controllers/get-user";
import usersList from "./controllers/users-list";
router.get("/users", usersList, {
middleware: [authMiddleware],
});
router.get("/users/:id", getUser);
router.post("/users", createUser);
router.post("/login", login);
Now open postman man, generate a guest
token using /guests
route, then update the Bearer
token with the generated token, then send the request to /users
route, you should see the logged guest model in console.
user() function
Now we've gotten the currentUser
model, we need to access it from anywhere in the application, so we'll create a user()
function to get the current user.
// src/core/auth/current-user.ts
import { Model } from "core/database";
let currentUser: Model | undefined;
/**
* Set current user
*/
export function setCurrentUser(model: Model | undefined) {
currentUser = model;
}
/**
* Get current user model
*/
export function user() {
return currentUser;
}
We created a file that a variable called currentUser
and two functions setCurrentUser
and user
to set and get the current user.
The currentUser
can have a Model
or undefined
value, and we created two functions for it to set and get the current user.
Now let's update the authMiddleware
to set the current user.
import config from "@mongez/config";
import { Request } from "core/http/request";
import { Response } from "core/http/response";
import { setCurrentUser } from "./current-user";
import jwt from "./jwt";
export async function authMiddleware(request: Request, response: Response) {
try {
// use our own jwt verify to verify the token
await jwt.verify();
// get current user
const user: any = request.baseRequest.user;
// now, we need to get an instance of user using its corresponding model
const userType = user.userType;
// get user model class
const UserModel = config.get(`auth.userType.${userType}`);
// get user model instance
const currentUser = await UserModel.findBy("_id", user._id);
// set current user
setCurrentUser(currentUser);
} catch (err) {
// unset current user
setCurrentUser(undefined);
return response.badRequest({
error: "Unauthorized: Invalid Access Token",
});
}
}
We set the current user in the try
block, and set it as undefined in the catch
block, why? it means the user's token is invalid so we need to make sure the user is undefined
when we use user
function.
Using user()
function
Now let's head to users-list
controller and use the user()
function.
// src/app/users/controllers/users-list.ts
import { user } from "core/auth/current-user";
import database from "core/database";
import { Request } from "core/http/request";
export default async function usersList(request: Request) {
const usersCollection = database.collection("users");
// log the current user
console.log(user());
const users = await usersCollection.find({}).toArray();
return {
users,
};
}
The beautiful thing is that, when we use the usersList
controller, always user()
function will return the current user model, why? because we won't allow accessing the controller if the token is not valid, that's why we made a return in the catch
block in the auth middleware
middleware.
🎨 Conclusion
In this chapter, we've made such an impressive progress, we created the current user and saw how to implement it and use it in our controllers, we also updated the crud-model
to make sure it receives the _id
properly.
☕♨️ Buy me a Coffee ♨️☕
If you like my articles and work, you can buy me a coffee, it will help me to keep going and keep creating more content.
🚀 Project Repository
You can find the latest updates of this project on Github
😍 Join our community
Join our community on Discord to get help and support (Node Js 2023 Channel).
🎞️ Video Course (Arabic Voice)
If you want to learn this course in video format, you can find it on Youtube, the course is in Arabic language.
📚 Bonus Content 📚
You may have a look at these articles, it will definitely boost your knowledge and productivity.
General Topics
- Event Driven Architecture: A Practical Guide in Javascript
- Best Practices For Case Styles: Camel, Pascal, Snake, and Kebab Case In Node And Javascript
- After 6 years of practicing MongoDB, Here are my thoughts on MongoDB vs MySQL
Packages & Libraries
- Collections: Your ultimate Javascript Arrays Manager
- Supportive Is: an elegant utility to check types of values in JavaScript
- Localization: An agnostic i18n package to manage localization in your project
React Js Packages
Courses (Articles)
Posted on November 17, 2022
Join Our Newsletter. No Spam, Only the good stuff.
Sign up to receive the latest update from our blog.