Your Web News in One Place

Help Webnuz

Referal links:

Sign up for GreenGeeks web hosting
October 25, 2021 03:35 pm GMT

Authentication based on clean architecture

First delivery...

In this new installment I share with you several use cases for authenticating to an API, with the @clean/scaffold package.

Use cases:

  • A user may be able to register.
  • A user can log in to the system through jwt authentication.

We install the package globally on our pc.

npm i -g @tsclean/scaffold

We create the project.

scaffold create:project --name=authentication

We create the entity with the corresponding attributes, in this case we are going to store a user.

scaffold create:entity --name=user

src/domain/models/user.ts

export type UserModel = {  id: string | number;  name: string;  email: string;  password: string;}export type AddUserParams = Omit<UserModel, 'id'>

Now we create the interface that will communicate the domain layer with the infrastructure layer. This interface will contain the use case.

Note: The interfaces when compiling the code to javascript are lost, for this reason to be able to apply the principle of Inversion of Dependencies, we must make reference in the communication of the components by means of a constant.

scaffold create:interface --name=add-user --path=models

src/domain/models/gateways/add-user-repository.ts

import {UserModel, AddUserParams} from "@/domain/models/user";export const ADD_USER_REPOSITORY = "ADD_USER_REPOSITORY";export interface IAddUserRepository {    addUser: (data: AddUserParams) => Promise<UserModel>;}

Now we create the service that is going to have all the logic to store the user.

scaffold create:service --name=add-user

Interface to communicate the service with external layers.

src/domain/use-cases/add-user-service.ts

import {AddUserParams} from "@/domain/models/user";export const ADD_USER_SERVICE = "ADD_USER_SERVICE";export interface IAddUserService {    addUser: (data: AddUserParams) => Promise<IAddUserService.Result | IAddUserService.Exist>}export namespace IAddUserService {    export type Exist = boolean;    export type Result = {        id?: string | number    }}

We create the business logic in the service, this involves applying some business rules.

src/domain/use-cases/impl/add-user-service-impl.ts

import {Adapter, Service} from "@tsclean/core";import {IAddUserService} from "@/domain/use-cases/add-user-service";import {AddUserParams} from "@/domain/models/user";import {ADD_USER_REPOSITORY, IAddUserRepository} from "@/domain/models/gateways/add-user-repository";@Service()export class AddUserServiceImpl implements IAddUserService {    constructor(        @Adapter(ADD_USER_REPOSITORY) private readonly addUserRepository: IAddUserRepository    ) {    }    async addUserService(data: AddUserParams): Promise<IAddUserService.Result | IAddUserService.Exist> {        return await this.addUserRepository.addUserRepository(data);    }}

This is the basic logic for the service to store the user, but we must check that the email is unique and create a hash for the password, so we get closer to a real world application.

We must create two interfaces for this purpose, one for the email validation and the other to create the password hash.

src/domain/models/gateways/check-email-repository.ts

export const CHECK_EMAIL_REPOSITORY = "CHECK_EMAIL_REPOSITORY";export interface ICheckEmailRepository {    checkEmail: (email: string) => Promise<ICheckEmailRepository.Result>}export namespace ICheckEmailRepository {    export type Result = {        id: string | number;        firstName: string;        password: string;    }}

src/domain/models/gateways/hash-repository.ts

export const HASH_REPOSITORY = "HASH_REPOSITORY";export interface IHashRepository {    hash: (text: string) => Promise<string>}

Now that the interfaces have been created to handle some of the business rules, we implement the interfaces in the service, passing them as a dependency in the constructor.

src/domain/use-cases/impl/add-user-service-impl.ts

import {Adapter, Service} from "@tsclean/core";import {IAddUserService} from "@/domain/use-cases/add-user-service";import {AddUserParams} from "@/domain/models/user";import {ADD_USER_REPOSITORY, IAddUserRepository} from "@/domain/models/gateways/add-user-repository";import {CHECK_EMAIL_REPOSITORY, ICheckEmailRepository} from "@/domain/models/gateways/check-email-repository";import {HASH_REPOSITORY, IHashRepository} from "@/domain/models/gateways/hash-repository";@Service()export class AddUserServiceImpl implements IAddUserService {    constructor(        @Adapter(HASH_REPOSITORY) private readonly hash: IHashRepository,        @Adapter(CHECK_EMAIL_REPOSITORY) private readonly checkEmailRepository: ICheckEmailRepository,        @Adapter(ADD_USER_REPOSITORY) private readonly addUserRepository: IAddUserRepository    ) {    }    async addUserService(data: AddUserParams): Promise<IAddUserService.Result | IAddUserService.Exist> {        const userExist = await this.checkEmailRepository.checkEmail(data.email);        if (userExist) return true;        const hashPassword = await this.hash.hash(data.password);        const user = await this.addUserRepository.addUserRepository({...data, password: hashPassword});        if (user) return user;    }}

Now we create the adapter in infrastructure layer.

scaffold create:adapter-orm --name=user --orm=mongoose

You must configure in the .env the url that you will use in the connection with mongoose.

Note: An update has been made in the plugin to give a management to the providers generating a single file, in this we include all the providers that are being created and by means of the spread operator we include them in the main container of the application so that the dependencies are solved.

src/infrastructure/driven-adapters/adapters/orm/mongoose/models/user.ts

import { model, Schema } from "mongoose";import { UserModel } from '@/domain/models/user';const schema = new Schema<UserModel>({    id: String,    firstName: String,    lastName: String,    email: String,    password: String,});export const UserModelSchema = model<UserModel>('users', schema);

src/infrastructure/driven-adapters/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 {// 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);    }}

Now we create the adapter of an external library to create the hash of the password, for this we use bcrypt, where we make the implementation of the interface, decoupling completely the components.

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

import bcrypt from "bcrypt";import {IHashRepository} from "@/domain/models/gateways/hash-repository";export class BcryptAdapter implements IHashRepository {    private readonly salt: number = 12;    constructor() {    }    async hash(text: string): Promise<string> {        return await bcrypt.hash(text, this.salt);    }}

src/infrastructure/driven-adapters/providers/index.ts

import {BcryptAdapter} from "@/infrastructure/driven-adapters/adapters/bcrypt-adapter";import {UserMongooseRepositoryAdapter} from "@/infrastructure/driven-adapters/adapters/orm/mongoose/user-mongoose-repository-adapter";import {AddUserServiceImpl} from "@/domain/use-cases/impl/add-user-service-impl";import {ADD_USER_REPOSITORY} from "@/domain/models/gateways/add-user-repository";import {CHECK_EMAIL_REPOSITORY} from "@/domain/models/gateways/check-email-repository";import {ADD_USER_SERVICE} from "@/domain/use-cases/add-user-service";import {HASH_REPOSITORY} from "@/domain/models/gateways/hash-repository";export const adapters = [    {        classAdapter: BcryptAdapter,        key: HASH_REPOSITORY    },    {        classAdapter: UserMongooseRepositoryAdapter,        key: ADD_USER_REPOSITORY    },    {        classAdapter: UserMongooseRepositoryAdapter,        key: CHECK_EMAIL_REPOSITORY    }]export const services = [    {        classAdapter: AddUserServiceImpl,        key: ADD_USER_SERVICE    }]

We create the controller as an entry point.

scaffold create:controller --name=add-user

src/infrastructure/entry-points/api/add-user-controller.ts

import {Mapping, Post, Body, Adapter} from "@tsclean/core";import {AddUserParams} from "@/domain/models/user";import {ADD_USER_SERVICE, IAddUserService} from "@/domain/use-cases/add-user-service";@Mapping('api/v1/add-user')export class AddUserController {    constructor(        @Adapter(ADD_USER_SERVICE) private readonly addUserService: IAddUserService    ) {    }    @Post()    async addUserController(@Body() data: AddUserParams): Promise<IAddUserService.Result | IAddUserService.Exist> {        return this.addUserService.addUserService(data);    }}

We are already validating in the use case that the email is unique, but it returns only a boolean value, we must handle this exception at the entry point, in this case the controller, in addition we validate that the email has the correct format and the body of the request does not bring empty fields.

To achieve this we create our own helper or we make use of an external library, if you make use of a library you must create the corresponding adapter for this purpose.

src/infrastructure/helpers/validate-fields.ts

export const REGEX = /^([a-zA-Z0-9_\.\-])+\@(([a-zA-Z0-9\-])+\.)+([a-zA-Z0-9]{2,4})+$/export class ValidateFields {    static fieldsValidation(data: any) {        let errors = {}        for (const key in data) {            if (ValidateFields.isFieldEmpty(data[key])) {                errors[key] = `${key} field is required`            } else if (key === "email" && !REGEX.test(data[key])) {                errors[key] = `${key} is invalid`            }        }        return { errors, isValid: ValidateFields.isFieldEmpty(errors) }    }    private static isFieldEmpty (value: any): boolean {        if (value === undefined || value === null ||            typeof value === "object" && Object.keys(value).length === 0 ||            typeof value === "string" && value.trim().length === 0) {            return true        }    }}

src/infrastructure/entry-points/api/add-user-controller.ts

import {Mapping, Post, Body, Adapter} from "@tsclean/core";import {AddUserParams} from "@/domain/models/user";import {ADD_USER_SERVICE, IAddUserService} from "@/domain/use-cases/add-user-service";import {ValidateFields} from "@/infrastructure/helpers/validate-fields";@Mapping('api/v1/add-user')export class AddUserController {    constructor(        @Adapter(ADD_USER_SERVICE) private readonly addUserService: IAddUserService    ) {    }    @Post()    async addUserController(@Body() data: AddUserParams): Promise<IAddUserService.Result | IAddUserService.Exist | any> {        const {errors, isValid} = ValidateFields.fieldsValidation(data);        if (!isValid) return {statusCode: 422, body: {"message": errors}}        const account = await this.addUserService.addUserService(data);        if (account === true) return {statusCode: 400, body: {"message": "Email is already in use"}}        return account;    }}

We create an index file to export all the controllers to the container.

src/infrastructure/entry-points/api/index.ts

import {AddUserController} from "@/infrastructure/entry-points/api/add-user-controller";export const controllers = [    AddUserController]

Finally we configure all the components in the main container

src/application/app.ts

import {Container} from "@tsclean/core";import {controllers} from "@/infrastructure/entry-points/api";import {adapters, services} from "@/infrastructure/driven-adapters/providers";@Container({    imports: [],    providers: [...services, ...adapters],    controllers: [...controllers],})export class AppContainer {}

The code looks much cleaner with the update that was made.

Previously when creating the adapter, the index.ts file that starts the application was updated with the necessary configuration to make the connection with the database manager.

src/index.ts

import 'module-alias/register'import helmet from 'helmet';import {connect} from 'mongoose';import {StartProjectServer} from "@tsclean/core";import {AppContainer} from "@/application/app";import {MONGODB_URI, PORT} from "@/application/config/environment";async function run(): Promise<void> {    await connect(MONGODB_URI);    console.log('DB Mongo connected')    const app = await StartProjectServer.create(AppContainer);    app.use(helmet());    await app.listen(PORT, () => console.log('Running on port: ' + PORT))}run();

next second delivery...


Original Link: https://dev.to/japhernandez/authentication-based-on-clean-architecture-1n74

Share this article:    Share on Facebook
View Full Article

Dev To

An online community for sharing and discovering great ideas, having debates, and making friends

More About this Source Visit Dev To