Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Feat/authorize #4

Merged
merged 12 commits into from
Jul 29, 2024
4 changes: 2 additions & 2 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,9 @@
name: Nest.JS clean architecture CI
on:
push:
branches: ['*']
branches: [main, develop]
pull_request:
branches: [main]
branches: [main, develop]
jobs:
lint-commit:
runs-on: ubuntu-latest
Expand Down
2 changes: 1 addition & 1 deletion .husky/pre-commit
Original file line number Diff line number Diff line change
@@ -1,2 +1,2 @@
pnpm lint-staged
pnpm lint-staged && pnpm test

2 changes: 1 addition & 1 deletion eslint.config.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,7 @@ export default tseslint.config(
'@typescript-eslint/no-explicit-any': 'off',
'no-unused-vars': 'off',
'@typescript-eslint/no-unused-vars': 'off',
'no-console': 'warn',
'no-console': 'error',
'@typescript-eslint/no-var-requires': 'off',
'@typescript-eslint/no-namespace': 'off',
},
Expand Down
4 changes: 1 addition & 3 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -82,9 +82,7 @@
"lint-staged": {
"*.ts": [
"pnpm lint",
"pnpm format",
"pnpm run test",
"pnpm run test:e2e"
"pnpm format"
]
}
}
46 changes: 46 additions & 0 deletions src/application/di/auth.module.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
import { AuthFacadeUsecase } from '@/domain/auth';
import { AuthorizeUsecase } from '@/domain/auth/usecases';
import { UserRepository } from '@/domain/user';
import { AuthController } from '@/presentation/controllers/auth';

import { UserModel } from '@/infrastructure/typeorm/models';
import { UserEntity } from '@/domain/user';
import { UserRepositoryImpl } from '@/infrastructure/typeorm/repositories';
import { Module } from '@nestjs/common';
import { Mapper } from '@/core';
import { UserMapper } from '@/infrastructure/typeorm/mappers';
import { CacheService } from '@/infrastructure/redis/cache';
import { TypeOrmModule } from '@nestjs/typeorm';

@Module({})
export class AuthFacadeModule {
static register() {
return {
module: AuthFacadeModule,
imports: [TypeOrmModule.forFeature([UserModel])],
providers: [
AuthFacadeUsecase,
AuthorizeUsecase,

{
provide: UserRepository,
useClass: UserRepositoryImpl,
},

{
provide: Mapper<UserEntity, UserModel>,
useClass: UserMapper,
},
],
exports: [AuthFacadeUsecase, AuthorizeUsecase],
};
}
}

@Module({
imports: [AuthFacadeModule.register()],
controllers: [AuthController],
providers: [CacheService],
exports: [],
})
export class AuthModule {}
2 changes: 2 additions & 0 deletions src/application/di/index.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
export { PresentationModule } from './presentation.module';
export { TypeOrmModule } from './typeorm.module';
export { AppModule } from './app.module';
export { AuthModule } from './auth.module';
export { UserModule } from './user.module';
4 changes: 2 additions & 2 deletions src/application/di/presentation.module.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
import { Module } from '@nestjs/common';
import { UserFacadeUsecase } from '@/domain/user';
import { UserModule } from './user.module';
import { AuthModule } from './auth.module';

@Module({
imports: [UserModule],
imports: [UserModule, AuthModule],
controllers: [],
providers: [],
})
Expand Down
22 changes: 14 additions & 8 deletions src/application/di/user.module.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,33 +5,33 @@ import { UserController } from '@/presentation/controllers/user';
import { Module } from '@nestjs/common';

import { TypeOrmModule } from '@nestjs/typeorm';
import { UserModel as TypeormUserModel } from '@/infrastructure/typeorm/models';
import { UserModel } from '@/infrastructure/typeorm/models';
import { UserEntity } from '@/domain/user';
import { UserRepositoryImpl as RepoTypeorm } from '@/infrastructure/typeorm/repositories';
import { UserRepositoryImpl } from '@/infrastructure/typeorm/repositories';

import { Mapper } from '@/core';
import { UserMapper as TypeormUserMapper } from '@/infrastructure/typeorm/mappers';
import { UserMapper } from '@/infrastructure/typeorm/mappers';

@Module({})
export class UserFacadeModule {
static register() {
return {
module: UserFacadeModule,

imports: [TypeOrmModule.forFeature([TypeormUserModel])],
imports: [TypeOrmModule.forFeature([UserModel])],
providers: [
UserFacadeUsecase,
CreateUserUsecase,
GetUserUsecase,

{
provide: UserRepository,
useClass: RepoTypeorm,
useClass: UserRepositoryImpl,
},

{
provide: Mapper<UserEntity, TypeormUserModel>,
useClass: TypeormUserMapper,
provide: Mapper<UserEntity, UserModel>,
useClass: UserMapper,
},
],
exports: [UserFacadeUsecase, CreateUserUsecase, GetUserUsecase],
Expand All @@ -42,7 +42,13 @@ export class UserFacadeModule {
@Module({
imports: [UserFacadeModule.register()],
controllers: [UserController],
providers: [CacheService],
providers: [
CacheService,
{
provide: Mapper<UserEntity, UserModel>,
useClass: UserMapper,
},
],
exports: [],
})
export class UserModule {}
9 changes: 9 additions & 0 deletions src/application/dtos/auth/authorize.dto.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
import { Length } from 'class-validator';

export class AuthorizeDto {
@Length(4, 20)
username: string;

@Length(6)
password: string;
}
2 changes: 1 addition & 1 deletion src/application/dtos/user/create-user.dto.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { IsNotEmpty, IsString, Length } from 'class-validator';
import { IsString, Length } from 'class-validator';

export class CreateUserDto {
@IsString()
Expand Down
6 changes: 3 additions & 3 deletions src/core/mapper.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
export abstract class Mapper<D, E> {
abstract toEntity(entity: Partial<E>): Partial<D>;
abstract toModel(domain: Partial<D>): Partial<E>;
export abstract class Mapper<E, M> {
abstract toEntity(entity: Partial<M>): Partial<E>;
abstract toModel(domain: Partial<E>): Partial<M>;
}
2 changes: 2 additions & 0 deletions src/core/repository.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,4 +10,6 @@ export abstract class Repository<TEntity extends Entity> {
): Promise<Partial<TEntity>>;
abstract delete(id: string): Promise<Partial<TEntity>>;
abstract create(data: Partial<TEntity>): Promise<Partial<TEntity>>;

abstract findOne(filter: Partial<TEntity>): Promise<Partial<TEntity>>;
}
2 changes: 1 addition & 1 deletion src/core/usecase.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,3 @@
export abstract class Usecase<T> {
export abstract class Usecase<T = unknown> {
abstract execute(...args: any[]): Promise<Partial<T>>;
}
15 changes: 15 additions & 0 deletions src/domain/auth/auth.facade.usecase.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
import { Inject, Injectable } from '@nestjs/common';
import { AuthorizeUsecase } from './usecases';
import { AuthorizeDto } from '@/application/dtos/auth/authorize.dto';

@Injectable()
export class AuthFacadeUsecase {
constructor(
@Inject(AuthorizeUsecase)
private readonly authorizeUsecase: AuthorizeUsecase,
) {}

authorize(payload: AuthorizeDto) {
return this.authorizeUsecase.execute(payload);
}
}
3 changes: 3 additions & 0 deletions src/domain/auth/auth.usecase.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
import { Usecase } from '@/core';

export abstract class AuthUsecase extends Usecase {}
2 changes: 2 additions & 0 deletions src/domain/auth/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
export { AuthUsecase } from './auth.usecase';
export { AuthFacadeUsecase } from './auth.facade.usecase';
65 changes: 65 additions & 0 deletions src/domain/auth/usecases/authorize.usecase.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
import { Test } from '@nestjs/testing';
import { AuthUsecase } from '../auth.usecase';
import { AuthorizeUsecase } from './authorize.usecase';
import { UserEntity, UserRepository } from '@/domain/user';
import { faker } from '@faker-js/faker';
import bcrypt from 'bcryptjs';

const mockPayload = {
username: faker.internet.userName(),
password: faker.internet.password(),
};
const mockUser = {
id: faker.string.uuid(),
username: mockPayload.username,
password: bcrypt.hashSync(mockPayload.password, 10),
createdAt: new Date(),
deletedAt: null,
updatedAt: new Date(),
};

describe('AuthorizeUsecase', () => {
let usecase: AuthUsecase;
let repository: UserRepository;

beforeEach(async () => {
const module = await Test.createTestingModule({
providers: [
AuthorizeUsecase,
{
provide: UserRepository,
useValue: {
findOne: jest.fn(),
},
},
],
}).compile();
usecase = module.get<AuthUsecase>(AuthorizeUsecase);
repository = module.get<UserRepository>(UserRepository);
});

it('should be defined', () => {
expect(usecase).toBeDefined();
expect(repository).toBeDefined();
});
it('should return null if user not found', async () => {
jest.spyOn(repository, 'findOne').mockResolvedValue(null);
expect(
await usecase.execute({ username: 'username', password: 'password' }),
).toBeNull();
});

it('should return user if user found', async () => {
jest.spyOn(repository, 'findOne').mockResolvedValue(mockUser);
const result = (await usecase.execute(mockPayload)) as UserEntity;
expect(result.id).toEqual(mockUser.id);
});
it('should return null if password is invalid', async () => {
jest.spyOn(repository, 'findOne').mockResolvedValue(mockUser);
const result = (await usecase.execute({
username: 'test',
password: 'wrong password',
})) as UserEntity;
expect(result).toEqual(null);
});
});
22 changes: 22 additions & 0 deletions src/domain/auth/usecases/authorize.usecase.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
import { AuthorizeDto } from '@/application/dtos/auth/authorize.dto';
import { AuthUsecase } from '@/domain/auth';
import { UserRepository } from '@/domain/user';
import { Injectable } from '@nestjs/common';
import bcrypt from 'bcryptjs';

@Injectable()
export class AuthorizeUsecase extends AuthUsecase {
constructor(private readonly userRepository: UserRepository) {
super();
}

async execute(payload: AuthorizeDto): Promise<Partial<any>> {
const { username, password } = payload;
const user = await this.userRepository.findOne({ username });

if (!user) return null;
const isValidPassword = bcrypt.compareSync(password, user.password);
if (!isValidPassword) return null;
return user;
}
}
1 change: 1 addition & 0 deletions src/domain/auth/usecases/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export { AuthorizeUsecase } from './authorize.usecase';
8 changes: 6 additions & 2 deletions src/domain/user/user.entity.ts
Original file line number Diff line number Diff line change
@@ -1,16 +1,20 @@
import { Entity } from '@/core/entity';
import { Exclude } from 'class-transformer';

export class UserEntity extends Entity {
@Exclude()
public password: string;
constructor(
public id: string,
public username: string,
password: string,

public createdAt: Date,
public updatedAt: Date,
public deletedAt: Date,

public password?: string,
) {
super();

this.password = password;
}
}
4 changes: 2 additions & 2 deletions src/domain/user/user.facade.usecase.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,11 +10,11 @@ export class UserFacadeUsecase {
@Inject(CreateUserUsecase) private readonly createUserUsecase: UserUsecase,
) {}

async createUser(data: Partial<UserEntity>) {
createUser(data: Partial<UserEntity>) {
return this.createUserUsecase.execute(data);
}

async getUser(id: string) {
getUser(id: string) {
return this.getUserUsecase.execute(id);
}
}
1 change: 1 addition & 0 deletions src/infrastructure/typeorm/mappers/user.mapper.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ export class UserMapper extends Mapper<UserEntity, UserModel> {
return new UserEntity(
data.id,
data.username,
data.password,
data.createdAt,
data.updatedAt,
data.deletedAt,
Expand Down
2 changes: 1 addition & 1 deletion src/infrastructure/typeorm/models/user.model.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ export class UserModel {
username: string;

@Column({
select: false,
select: true,
})
password?: string;

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,14 @@ export class UserRepositoryImpl extends UserRepository {
) {
super();
}

async findOne(filter: Partial<UserEntity>): Promise<Partial<UserEntity>> {
const userTypeorm = await this.userRepository.findOne({
where: filter,
});
return this.userMapper.toEntity(userTypeorm);
}

async findUnique(id: string) {
this.logger.log('[Typeorm] findUnique', id);
if (!uuid.validate(id)) return null;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -159,4 +159,12 @@ describe('UserRepository', () => {
const result = await repository.findMany({});
expect(result).toEqual(users);
});
it('should return only one user', async () => {
const user = mockUser;
jest
.spyOn(userRepository, 'findOne')
.mockImplementation(() => Promise.resolve(user));
const result = await repository.findOne({ username: user.username });
expect(result).toEqual(user);
});
});
Loading
Loading