DEV Community

John Piedrahita
John Piedrahita

Posted on • Edited on

Third part, authentication based on clean architecture

Third part...

In the previous post we made the use case to authenticate ourselves in the system, now we are going to solve a use case to read all the users that exist in the system and include the roles so they can access this resource.

  1. Use case to read the system users

We create the gateway for this use case.

scaffold create:interface --name=get-users --path=models
Enter fullscreen mode Exit fullscreen mode

We create this interface to then apply the principle of Dependency Inversion, between the service and the Mongo DB adapter.

src/domain/models/gateways/get-users-repository.ts

import {UserModel} from "@/domain/models/user";

export const GET_USERS_REPOSITORY = "GET_USERS_REPOSITORY";

export interface IGetUsersRepository {
    getUsersRepository: () => Promise<UserModel[]>
}
Enter fullscreen mode Exit fullscreen mode

Now we create the service that will implement the logic of the use case.

scaffold create:service --name=get-users
Enter fullscreen mode Exit fullscreen mode

src/domain/use-cases/get-users-service.ts

We create this interface to then apply the principle of Dependency Inversion, between the service and the entry point.

import {UserModel} from "@/domain/models/user";

export const GET_USERS_SERVICE = "GET_USERS_SERVICE";

export interface IGetUsersService {
    getUsersService: () => Promise<UserModel[]>
}
Enter fullscreen mode Exit fullscreen mode

src/domain/use-cases/impl/get-users-service-impl.ts

import {Adapter, Service} from "@tsclean/core";

@Service()
export class GetUsersServiceImpl implements IGetUsersService {
    constructor(
        @Adapter(GET_USERS_REPOSITORY) private readonly getUsersRepository: IGetUsersRepository
    ) {
    }

    async getUsersService(): Promise<UserModel[]> {
        return await this.getUsersRepository.getUsersRepository();
    }
}
Enter fullscreen mode Exit fullscreen mode

We already have everything we need in the domain layer for the use case, now we are going to include the interface in the mongo adapter we have.

src/infrastructure/driven-adapters/orm/mongoose/user-mongoose-repository-adapter.ts

import {AddUserParams, UserModel} from "@/domain/models/user";
import {UserModelSchema} from "@/infrastructure/driven-adapters/adapters/orm/mongoose/models/user";
import {IAddUserRepository} from "@/domain/models/gateways/add-user-repository";
import {ICheckEmailRepository} from "@/domain/models/gateways/check-email-repository";

export class UserMongooseRepositoryAdapter implements IAddUserRepository,
ICheckEmailRepository,
IGetUsersRepository {

// We create this function to manage the entity that exists in the domain.
    map(data: any): any {
        const {_id, firstName, lastName, email, password} = data
        return Object.assign({}, {id: _id.toString(), firstName, lastName, email, password})
    }

    async addUserRepository(data: AddUserParams): Promise<UserModel> {
        return await UserModelSchema.create(data);
    }

    async checkEmail(email: string): Promise<ICheckEmailRepository.Result> {
        const user = await UserModelSchema.findOne({email}).exec();
        return user && this.map(user);
    }

    async getUsersRepository(): Promise<UserModel[]> {
        return UserModelSchema.find().select("-password");
    }
}
Enter fullscreen mode Exit fullscreen mode

We create the controller as an entry point.

scaffold create:controller --name=get-users
Enter fullscreen mode Exit fullscreen mode

src/infrastructure/entry-points/api/get-users-controller.ts

import {Adapter, Get, Mapping} from "@tsclean/core";
import {GET_USERS_SERVICE, IGetUsersService} from "@/domain/use-cases/get-users-service";

@Mapping('api/v1/get-users')
export class GetUsersController {

    constructor(
        @Adapter(GET_USERS_SERVICE) private readonly getUsersService: IGetUsersService
    ) {
    }

    @Get()
    async getUsersController(): Promise<any> {
        return await this.getUsersService.getUsersService();
    }
}
Enter fullscreen mode Exit fullscreen mode

If everything went well and you have reached this point when you visit the endpoint http://localhost:9000/api/v1/get-users, it should users list.

  1. Protecting resources with roles.

We have already solved our use cases, now we are going to protect some resources so that only users with a certain role can access them. To achieve this we must first go to the adapter and create the corresponding logic.

src/infrastructure/driven-adapters/adapters/jwt-adapter.ts

import jwt from "jsonwebtoken";
import dotenv from "dotenv";
import {IEncrypt} from "@/domain/models/gateways/encrypt-repository";
import {AccessResourceInterface, ExecutionContextInterface} from "@tsclean/core";

dotenv.config({path: ".env"})

export class JwtAdapter implements IEncrypt, AccessResourceInterface {

    constructor(
        private readonly roles: string[]
    ) {
    }

    async encrypt(text: string | number | Buffer, roles: []): Promise<string> {
        const payload = {id: text, roles: roles}
        return jwt.sign({account: payload}, process.env.JWT_SECRET, {expiresIn: "1d"});
    }

    accessResource(context: ExecutionContextInterface): boolean | Promise<boolean> {
        try {
            const request = context.getHttp().getRequest();
            const token = request.rawHeaders[1].split(" ")[1];

            if (token) {
                const decode = jwt.verify(token, process.env.JWT_SECRET);
                const roles = decode["account"].roles;

                let assignRole = false;

                for (const role of roles) {
                    assignRole = false;
                    for (const roleElement of this.roles) {
                        let roleExist = role.role === roleElement;
                        if (roleExist) assignRole = roleExist;
                        if (assignRole) return true;
                    }
                }
            }
        } catch (e) {
            return false;
        }
    }
}
Enter fullscreen mode Exit fullscreen mode

What we do in this adapter is to implement the AccessResourceInterface interface provided by the plugin with its accessResource method. We receive through the constructor the roles of the user that tries to access the resource. And a boolean is returned according to the validation.

Next we create a factory that will help us in the controller with this validation.

src/infrastructure/helpers/auth.ts

import {applyDecorators, AccessResource} from "@tsclean/core";
import {JwtAdapter} from "@/infrastructure/driven-adapters/adapters/jwt-adapter";

export function Auth(roles: string[]) {
    return applyDecorators(AccessResource(new JwtAdapter(roles)));
}
Enter fullscreen mode Exit fullscreen mode

src/infrastructure/entry-points/api/get-users-controller.ts

import {Adapter, Get, Mapping} from "@tsclean/core";
import {Auth} from "@/infrastructure/helpers/auth";
import {GET_USERS_SERVICE, IGetUsersService} from "@/domain/use-cases/get-users-service";

@Mapping('api/v1/get-users')
export class GetUsersController {

    constructor(
        @Adapter(GET_USERS_SERVICE) private readonly getUsersService: IGetUsersService
    ) {
    }

    @Get()
    @Auth(["manager", "guest"])
    async getUsersController(): Promise<any> {
        return await this.getUsersService.getUsersService();
    }
}
Enter fullscreen mode Exit fullscreen mode

Now you can see that the method has an additional decorator that receives an array of the roles that can access the resource. This resource can have one or many roles.

When a user tries to access the resource with a role different from those in the decorator, it returns a 403 status code. With this, we can protect the ends in a simple way.

If you have reached this point, you have the way to create the authentication for a system and protect the routes with roles and all this applying a concept based on Clean Architecture.

In this link you can find the implementation of the project https://github.com/tsclean/api-example

Top comments (0)