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(CC-88): created authenticate endpoint #89

Merged
merged 10 commits into from
Nov 7, 2023
15 changes: 9 additions & 6 deletions .github/workflows/tests-pipeline.yml
Original file line number Diff line number Diff line change
Expand Up @@ -43,13 +43,16 @@ jobs:
run: |
echo "DATABASE_URL=postgresql://${{ secrets.POSTGRES_USER }}:${{ secrets.POSTGRES_PASSWORD }}@localhost:5432/${{ secrets.POSTGRES_DB }}?schema=${{ secrets.POSTGRES_SCHEMA }}" >> $GITHUB_ENV

- name: Setting up docker compose
run: docker compose up -d
# - name: Setting up docker compose
# run: docker compose up -d

- name: Setting up prisma postgresql database
run: |
pnpm run prisma:core:schema:generate
pnpm run prisma:core:schema:migrate
# - name: Setting up prisma postgresql database
# run: |
# pnpm run prisma:core:schema:generate
# pnpm run prisma:core:schema:migrate

- name: Run core-setup script
run: pnpm run core-setup --action=migrate,generate

- name: Run E2E Tests
run: pnpm run test:e2e
Expand Down
4 changes: 4 additions & 0 deletions .vscode/settings.json
Original file line number Diff line number Diff line change
Expand Up @@ -14,8 +14,12 @@
"commitlint",
"conventionalcommits",
"CQRS",
"encrypter",
"Encrypter",
"github",
"GITHUB",
"hasher",
"Hasher",
"instanceof",
"italo",
"keyof",
Expand Down
31 changes: 20 additions & 11 deletions libs/core-rest-api/adapters/src/auth/auth.module.ts
Original file line number Diff line number Diff line change
@@ -1,33 +1,42 @@
import { Module } from '@nestjs/common';
import { ConfigService } from '@nestjs/config';
import { APP_GUARD } from '@nestjs/core';
import { JwtModule } from '@nestjs/jwt';
import { PassportModule } from '@nestjs/passport';

import { Env } from '../env/env';
import { EnvService } from '../env/env.service';
import { JwtAuthGuard } from './jwt-auth.guard';
import { JwtStrategy } from './jwt.strategy';


@Module({
imports: [
PassportModule,
JwtModule.registerAsync({
inject: [ConfigService],
global: true,
useFactory(config: ConfigService<Env, true>){
const privateKey = config.get('JWT_PRIVATE_KEY', {infer: true});
const publicKey = config.get('JWT_PUBLIC_KEY', {infer: true});
useFactory(config: ConfigService<Env, true>) {
const privateKey = config.get('JWT_PRIVATE_KEY', { infer: true });
const publicKey = config.get('JWT_PUBLIC_KEY', { infer: true });

return {
signOptions: {
algorithm: 'RS256', expiresIn: '1h'
algorithm: 'RS256',
expiresIn: '1h',
},
privateKey: Buffer.from(privateKey, 'base64'),
publicKey: Buffer.from(publicKey, 'base64')
}
}
})
publicKey: Buffer.from(publicKey, 'base64'),
};
},
}),
],
providers: [
JwtStrategy,
EnvService,
{
provide: APP_GUARD,
useClass: JwtAuthGuard,
},
],
providers: [JwtStrategy, EnvService]
})
export class AuthModule{}
export class AuthModule {}
34 changes: 33 additions & 1 deletion libs/core-rest-api/adapters/src/auth/jwt-auth.guard.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,35 @@
/* eslint-disable @typescript-eslint/no-explicit-any */
import { ExecutionContext, Injectable, UnauthorizedException } from '@nestjs/common';
import { Reflector } from '@nestjs/core';
import { AuthGuard } from '@nestjs/passport';
import { IS_PUBLIC_KEY } from './public';

export class JwtAuthGuard extends AuthGuard('jwt'){}
@Injectable()
export class JwtAuthGuard extends AuthGuard('jwt') {
constructor(private reflector: Reflector) {
super();
}

override canActivate(context: ExecutionContext) {
console.log('[JWT-GUARD] - Verifying if route is public');

const isPublic = this.reflector.getAllAndOverride<boolean>(IS_PUBLIC_KEY, [
context.getHandler(),
context.getClass(),
]);

if (!isPublic) {
return super.canActivate(context);
}

return true;
}

override handleRequest(err: unknown, user: any) {
if (err || !user) {
throw new UnauthorizedException('Invalid JWT token');
}

return user;
}
}
24 changes: 13 additions & 11 deletions libs/core-rest-api/adapters/src/auth/jwt.strategy.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,26 +5,28 @@ import { z } from 'zod';
import { EnvService } from '../env/env.service';

const tokenPayloadSchema = z.object({
sub: z.string().uuid()
})
id: z.string().uuid(),
name: z.string(),
email: z.string().email(),
});

export type TokenPayload = z.infer<typeof tokenPayloadSchema>
export type TokenPayload = z.infer<typeof tokenPayloadSchema>;

@Injectable()
export class JwtStrategy extends PassportStrategy(Strategy){
constructor(config: EnvService){
const publicKey = config.get('JWT_PUBLIC_KEY')
console.log('JwtStrategy')
export class JwtStrategy extends PassportStrategy(Strategy) {
constructor(config: EnvService) {
const publicKey = config.get('JWT_PUBLIC_KEY');

super({
jwtFromRequest: ExtractJwt.fromAuthHeaderAsBearerToken(),
secretOrKey: Buffer.from(publicKey, 'base64'),
algorithms: ['RS256']
})
algorithms: ['RS256'],
});
}

async validate(payload: TokenPayload) {
console.log('jwtStrategy validate')
console.log('jwtStrategy validate');

return tokenPayloadSchema.parse(payload)
return tokenPayloadSchema.parse(payload);
}
}
4 changes: 4 additions & 0 deletions libs/core-rest-api/adapters/src/auth/public.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
import { SetMetadata } from '@nestjs/common';

export const IS_PUBLIC_KEY = 'isPublic';
export const Public = () => SetMetadata(IS_PUBLIC_KEY, true);
26 changes: 18 additions & 8 deletions libs/core-rest-api/adapters/src/controllers/api/api.module.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import { BcryptHasherService } from '@clinicControl/core-rest-api/core/src/shared/cryptography/use-cases/bcrypt-hasher.service';
import { Module } from '@nestjs/common';
import { ConfigModule } from '@nestjs/config';
import { AuthModule } from '../../auth/auth.module';
Expand All @@ -6,21 +7,30 @@ import { PostgreSqlPrismaOrmService } from '../../database/infra/prisma/prisma.s
import { DatabaseRepositoriesModule } from '../../database/repositories/repositories.module';
import { envSchema } from '../../env/env';
import { EnvModule } from '../../env/env.module';
import { CreatePsychologistController } from './use-case/create-psychologist/create-psychologist.controller';
import { NestjsCreatePsychologistService } from './use-case/create-psychologist/nestjs-create-psychologist.service';

import { AuthenticatePsychologistController } from './use-cases/psychologist/authenticate-psychologist/authenticate-psychologist.controller';
import { CreatePsychologistController } from './use-cases/psychologist/create-psychologist/create-psychologist.controller';

import { NestjsAuthenticatePsychologistService } from './use-cases/psychologist/authenticate-psychologist/nestjs-authenticate-psychologist.service';
import { NestjsCreatePsychologistService } from './use-cases/psychologist/create-psychologist/nestjs-create-psychologist.service';

@Module({
imports: [
DatabaseRepositoriesModule,
EnvModule,
AuthModule,
CryptographyModule,
ConfigModule.forRoot({
validate: (env) => envSchema.parse(env),
isGlobal: true,
}),
DatabaseRepositoriesModule,
EnvModule,
AuthModule,
CryptographyModule,
],
controllers: [CreatePsychologistController, AuthenticatePsychologistController],
providers: [
BcryptHasherService,
PostgreSqlPrismaOrmService,
NestjsCreatePsychologistService,
NestjsAuthenticatePsychologistService,
],
controllers: [CreatePsychologistController],
providers: [PostgreSqlPrismaOrmService, NestjsCreatePsychologistService],
})
export class ApiModule {}
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
/*
@see https://docs.nestjs.com/fundamentals/execution-context

- Como é um decorator ele começa com a letra maiúscula
- O decorator CurrentUser é um decorator de parâmetro de método e não de classe, por isso usamos createParamDecorator
- O decorator CurrentUser recebe dois parâmetros:
- O primeiro é o nome do parâmetro que queremos injetar, mas como não queremos injetar nenhum parâmetro, passamos um underscore
- O segundo é o contexto da requisição
*/

import { ExecutionContext, createParamDecorator } from '@nestjs/common';
import { TokenPayload } from '../../../auth/jwt.strategy';

export const CurrentUser = createParamDecorator((_: never, context: ExecutionContext) => {
const request = context.switchToHttp().getRequest(); // usamos o switchToHttp para ter acesso ao request da requisição, e getRequest() para pegar o request (o request é um objeto que tem várias informações da requisição)

return request.user as TokenPayload;
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
import { Body, Controller, Post } from '@nestjs/common';
import { ApiTags } from '@nestjs/swagger';

import { AuthenticatePsychologistDto } from '@clinicControl/core-rest-api/core/src/domains/psychologist/use-cases/authenticate-psychologist/authenticate-psychologist-dto';
import { Encrypter } from '@clinicControl/core-rest-api/core/src/shared/cryptography/repository/encrypter-repository';
import { GlobalAppHttpException } from '@clinicControl/core-rest-api/core/src/shared/errors/globalAppHttpException';

import { Public } from '../../../../../auth/public';
import { AuthenticatePsychologistControllerResponse } from './authenticate-psychologist.interface';
import { NestjsAuthenticatePsychologistService } from './nestjs-authenticate-psychologist.service';

@ApiTags()
@Controller({
path: 'psychologist',
})
export class AuthenticatePsychologistController {
constructor(
private AuthenticatePsychologistService: NestjsAuthenticatePsychologistService,
private jwtEncrypter: Encrypter
) {}

@Post('login')
@Public()
async execute(
@Body() psychologistLoginDto: AuthenticatePsychologistDto
): Promise<AuthenticatePsychologistControllerResponse> {
try {
const { id, name, email } = await this.AuthenticatePsychologistService.execute(
psychologistLoginDto
);

const access_token = await this.jwtEncrypter.encrypt({ id, name, email });

return { user: { id, name, email }, access_token };
} catch (error: unknown) {
throw new GlobalAppHttpException(error);
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
import request from 'supertest';

import { INestApplication } from '@nestjs/common';
import { Test } from '@nestjs/testing';

import { fakerPT_BR as faker } from '@faker-js/faker';
import { hash } from 'bcryptjs';

import { DatabaseRepositoriesModule } from '@clinicControl/core-rest-api/adapters/src/database/repositories/repositories.module';
import { PsychologistFactory } from '../../../../../../tests/factories/make-psychologist';
import { ApiModule } from '../../../api.module';

describe('[E2E] - Authenticate Psychologist', () => {
let app: INestApplication;
let psychologistFactory: PsychologistFactory;

beforeAll(async () => {
const moduleRef = await Test.createTestingModule({
imports: [ApiModule, DatabaseRepositoriesModule],
providers: [PsychologistFactory],
}).compile();

app = moduleRef.createNestApplication();

psychologistFactory = moduleRef.get(PsychologistFactory);

await app.init();
});

it('[POST] - Should successfully authenticate a psychologist', async () => {
const password = faker.internet.password({ length: 8 });
const newPsychologist = await psychologistFactory.makePrismaPsychologist({
password: await hash(password, 8),
});

const response = await request(app.getHttpServer()).post('/psychologist/login').send({
email: newPsychologist.email,
password,
});

expect(response.statusCode).toBe(201);
expect(response.body).toEqual({
user: {
id: newPsychologist.id,
name: newPsychologist.name,
email: newPsychologist.email,
},
access_token: expect.any(String),
});
});

it('[POST] - Should return an error 401 when trying to authenticate with wrong password', async () => {
const password = faker.internet.password({ length: 8 });
const newPsychologist = await psychologistFactory.makePrismaPsychologist({
password: await hash(password, 8),
email: '[email protected]',
});

const response = await request(app.getHttpServer())
.post('/psychologist/login')
.send({
email: newPsychologist.email,
password: faker.internet.password({ length: 3 }),
});

expect(response.statusCode).toBe(401);
});

it('[POST] - Should return an error 401 when trying to authenticate with wrong password', async () => {
const password = faker.internet.password({ length: 8 });

const response = await request(app.getHttpServer()).post('/psychologist/login').send({
email: '[email protected]',
password,
});

expect(response.statusCode).toBe(401);
});
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
export interface AuthenticatePsychologistControllerResponse {
user: {
id: string;
name: string;
email: string;
};
access_token: string;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
import { PsychologistDatabaseRepository } from '@clinicControl/core-rest-api/core/src/domains/psychologist/repositories/database-repository';
import { AuthenticatePsychologistService } from '@clinicControl/core-rest-api/core/src/domains/psychologist/use-cases/authenticate-psychologist/authenticate-psychologist.service';
import { Injectable } from '@nestjs/common';

@Injectable()
export class NestjsAuthenticatePsychologistService extends AuthenticatePsychologistService {
constructor(psychologistDatabaseRepository: PsychologistDatabaseRepository) {
super(psychologistDatabaseRepository);
}
}
Loading
Loading