68-Nodejs Course 2023: Auth: Base Auth Model Class

hassanzohdy

Hasan Zohdy

Posted on November 19, 2022

68-Nodejs Course 2023: Auth: Base Auth Model Class

So we saw how to generate an access token when the guest performs a login, let's see a quick peek at the code.

// src/app/users/controllers/auth/login.ts
import User from "app/users/models/user";
import jwt from "core/auth/jwt";
import { Request } from "core/http/request";
import { Response } from "core/http/response";

export default async function login(request: Request, response: Response) {
  // get the email and password from the request body
  const { email, password } = request.only(["email", "password"]);

  const user = await User.attempt({ email, password });

  if (!user) {
    return response.badRequest({
      error: "Invalid credentials",
    });
  }

  // generate access token
  const token = await jwt.generate({
    ...user.only(["id", "_id"]),
    userType: "user",
  });

  return response.success({
    user: user.data,
    // send the access token to the client
    accessToken: token,
    // send the user type to the client
    userType: "user",
  });
}
Enter fullscreen mode Exit fullscreen mode

We did also make it with guests as well, let's have a look

// 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

You'll notice that, we forgot in the login to store the token in AccessToken model, also there is a duplicate in the code when generating a jwt token.

Let's encapsulate the code with Auth model class.

// src/core/auth/models/auth.ts
import { Model } from "core/database";

export default abstract class Auth extends Model {
  /**
   * Get user type
   */
  public abstract get userType(): string;

  /**
   * Generate jwt token
   */
  public async generateAccessToken(): Promise<string> {
    //

    return "";
  }
}
Enter fullscreen mode Exit fullscreen mode

Here we used the power of abstract concept (You can find it in depth in the OOP Course, link in the article tail), to enforce that this class can not be instantiated, and any child must implement a getter method userType to return the user type of its own.

Then we defined generateAccessToken method, but it is so far has empty body with an empty string returned, this method should generate an access token, store it in the AccessToken model, and return that token.

// src/core/auth/models/auth.ts
import { Model } from "core/database";
import jwt from "../jwt";
import AccessToken from "./access-token";

export default abstract class Auth extends Model {
  /**
   * Get user type
   */
  public abstract get userType(): string;

  /**
   * Generate jwt token
   */
  public async generateAccessToken(): Promise<string> {
    //
    // store the main data in the data object
    // we need to store the user data in an object
    // that we'll sue to generate the token
    // and also it will be saved in the Access Token model under `user` column
    const data = {
      ...this.only(["id", "_id"]),
      userType: this.userType,
    };

    // use our own jwt generator to generate a token for the guest
    const token = await jwt.generate(data);

    // store token and the auth model data in the access token model
    // note that we didn't make it sync because we don't want to wait for the token to be stored in the database
    // as nothing depends on it
    AccessToken.create({
      token,
      user: data,
    });

    return token;
  }
}
Enter fullscreen mode Exit fullscreen mode

Here we created an object that collects the main data that we need, the id, _id and the userType, then we used our own jwt generator to generate a token for our current user, after that we stored the token and the auth model data in the access token model.

We grouped the data under user field so we don't override the _id or id of the AccessToken collection itself.

Let's now make our Guest extends Auth class.

// src/core/auth/models/guest.ts
import Auth from "./auth";

export default class Guest extends Auth {
  /**
   * {@inheritDoc}
   */
  public static collectionName = "guests";

  /**
   * Get user type
   */
  public get userType(): string {
    return "guest";
  }
}
Enter fullscreen mode Exit fullscreen mode

We extended the Auth class, and implemented the userType getter method to return the user type of the guest.

Now let's update our registerAuthRoutes function to use the new generateAccessToken method.

// src/core/auth/registerAuthRoutes.ts
import { Response } from "core/http/response";
import router from "core/router";
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 guest.generateAccessToken();

    return response.send({
      accessToken: token,
      // return the user type
      userType: guest.userType,
    });
  });
}
Enter fullscreen mode Exit fullscreen mode

Now our code is much cleaner and more readable, and we can use the same method in our User model.

// src/app/users/models/user.ts
import { verify } from "@mongez/password";
import { except } from "@mongez/reinforcements";
import Auth from "core/auth/models/auth";
import castPassword from "core/database/casts/cast-password";
import { Casts, Document } from "core/database/model/types";

export default class User extends Auth {
  /**
   * Collection name
   */
  public static collectionName = "users";

  /**
   * Get user type
   */
  public get userType(): string {
    return "user";
  }

  /**
   * {@inheritDoc}
   */
  public defaultValue: Document = {
    isActive: true,
    isEmailVerified: false,
    isPhoneVerified: false,
  };

  protected casts: Casts = {
    isActive: "boolean",
    isPhoneVerified: "boolean",
    joinDate: "date",
    password: castPassword,
  };

  /**
   * Attempt to login the user
   */
  public static async attempt(data: any) {
    // find first user with the given data, but exclude from it the password
    const user = await this.first(except(data, ["password"]));

    if (!user) {
      return null;
    }

    // now verify the password

    if (!verify(user.get("password"), data.password)) {
      return null;
    }

    return user;
  }
}
Enter fullscreen mode Exit fullscreen mode

We extended the Auth class, and implemented the userType getter method to return the user type of the user.

Actually you know what, the attempt method can be used with multiple users, so let's move it to the Auth class.

// src/core/auth/models/auth.ts
// ...
  /**
   * Attempt to login the user
   */
  public static async attempt<T>(
    this: ChildModel<T>,
    data: any,
  ): Promise<T | null> {
    // find first user with the given data, but exclude from it the password
    const user = await this.first(except(data, ["password"]));

    if (!user) {
      return null;
    }

    // now verify the password

    if (!verify((user as Model).get("password"), data.password)) {
      return null;
    }

    return user;
  }
Enter fullscreen mode Exit fullscreen mode

Here we moved the attempt method to the Auth class, we also as you can remember, did update the definition of this in the static method so we can return a proper return type when the login attempt is successful.

Now let's update our login route to use the new generateAccessToken method.

// src/app/users/controllers/auth/login.ts
import User from "app/users/models/user";
import { Request } from "core/http/request";
import { Response } from "core/http/response";

export default async function login(request: Request, response: Response) {
  // get the email and password from the request body
  const { email, password } = request.only(["email", "password"]);

  const user = await User.attempt({ email, password });

  if (!user) {
    return response.badRequest({
      error: "Invalid credentials",
    });
  }

  const token = user.generateAccessToken();

  return response.success({
    user: user.data,
    // send the access token to the client
    accessToken: token,
    // send the user type to the client
    userType: user.userType,
  });
}
Enter fullscreen mode Exit fullscreen mode

We did exact same thing here, we used the attempt method to attempt to login the user, and if the login attempt is successful, we used the generateAccessToken method to generate a token for the user.

Also don't forget that we return the userType using the userType getter method.

🎨 Conclusion

We created an Auth base model so we can extend from, and we used the generateAccessToken method to generate a token for the user, and we moved the attempt method to attempt from the User model to the Auth model so we can use it with multiple users.

☕♨️ Buy me a Coffee ♨️☕

If you enjoy my articles and see it useful to you, you may 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 19, 2022

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

Sign up to receive the latest update from our blog.

Related