66-Nodejs Course 2023: Auth: Current User

hassanzohdy

Hasan Zohdy

Posted on November 17, 2022

66-Nodejs Course 2023: Auth: Current User

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;
Enter fullscreen mode Exit fullscreen mode

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";
}
Enter fullscreen mode Exit fullscreen mode

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,
});
Enter fullscreen mode Exit fullscreen mode

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"),
    });
  });

  // ...
}
Enter fullscreen mode Exit fullscreen mode

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);
  }
Enter fullscreen mode Exit fullscreen mode

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;
}
Enter fullscreen mode Exit fullscreen mode

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;
  }
}
Enter fullscreen mode Exit fullscreen mode

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"),
    });
  });
}
Enter fullscreen mode Exit fullscreen mode

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",
    });
  }
}
Enter fullscreen mode Exit fullscreen mode

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);
Enter fullscreen mode Exit fullscreen mode

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;
}
Enter fullscreen mode Exit fullscreen mode

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",
    });
  }
}
Enter fullscreen mode Exit fullscreen mode

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,
  };
}
Enter fullscreen mode Exit fullscreen mode

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

Packages & Libraries

React Js Packages

Courses (Articles)

💖 💪 🙅 🚩
hassanzohdy
Hasan Zohdy

Posted on November 17, 2022

Join Our Newsletter. No Spam, Only the good stuff.

Sign up to receive the latest update from our blog.

Related