First part, Authentication based on clean architecture
Published: 2021-10-25 15:35:03

#### _First part..._

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_

```typescript
export type UserModel = {
  id: string | number;
  name: string;
  email: string;
  password: string;
  roles: UserRoleModel[];
}

export type UserRoleModel = [
    {
        role: 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_

```typescript
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_

```typescript
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_

```typescript
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_

```typescript
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_

```typescript
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_

```typescript
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_

```typescript
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_

```typescript
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_

```typescript
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_

```typescript
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_

```typescript
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_

```typescript
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_

```typescript
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_

```typescript
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_

```typescript
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_

```typescript
import 'module-alias/register'

import helmet from 'helmet';
import {connect} from 'mongoose';
import {StartProjectInit} 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 StartProjectInit.create(AppContainer);
    app.use(helmet());
    await app.listen(PORT, () => console.log('Running on port: ' + PORT))
}

run();
```

#### _next second part..._


https://dev.to/japhernandez/authentication-based-on-clean-architecture-38el