Skip to content

Commit

Permalink
Merge pull request #31 from viniciuscosmome/feature/authorization
Browse files Browse the repository at this point in the history
[LOR 176] feature: authorization
  • Loading branch information
Daaaiii committed Oct 31, 2023
2 parents 367d985 + eb61c7c commit a090e36
Show file tree
Hide file tree
Showing 16 changed files with 242 additions and 66 deletions.
3 changes: 2 additions & 1 deletion src/app.module.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,9 @@
import { Module } from '@nestjs/common';
import configModule from 'src/globals/constants';

import { AccountModule } from './modules';

@Module({
imports: [AccountModule],
imports: [configModule, AccountModule],
})
export class AppModule {}
24 changes: 21 additions & 3 deletions src/globals/constants.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
import { ConfigModule } from '@nestjs/config';

export const appName = 'LoryBlu';
export const appDescription = process.env.npm_package_description;
export const appVersion = process.env.npm_package_version;
Expand All @@ -6,6 +8,22 @@ export const appLicense = process.env.npm_package_license;
export const fullnameRegExp = /^[a-zÀ-ÿ ]+$/i;
export const recoveryTokenRegExp = /^[a-zA-Z0-9_-]+$/;

export const isDevelopmentEnv = process.env.NODE_ENV === 'development';
export const isHomologationEnv = process.env.NODE_ENV === 'homologation';
export const isProductionEnv = process.env.NODE_ENV === 'production';
export const constants = () => ({
PORT: process.env.PORT || 5500,
NODE_ENV: process.env.NODE_ENV,

SALT_DATA_HASH: process.env.SALT_DATA_HASH,
SALT_DATA_PASS: process.env.SALT_DATA_PASS,
SECRET_JWT: process.env.SECRET_JWT,

MAIL_API_KEY: process.env.MAIL_API_KEY,
MAIL_FROM: process.env.MAIL_FROM,
MAIL_TEST_DELIVERED: process.env.MAIL_TEST_DELIVERED,
MAIL_TEST_BOUNCED: process.env.MAIL_TEST_BOUNCED,
MAIL_TEST_COMPLAINED: process.env.MAIL_TEST_COMPLAINED,
});

export default ConfigModule.forRoot({
load: [() => constants],
isGlobal: true,
});
25 changes: 23 additions & 2 deletions src/globals/errors.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,10 @@
import { Prisma } from '@prisma/client';
import { isProductionEnv } from './constants';
import { constants } from './constants';
import { UnknownErrorException, P2002Exception } from './responses/exceptions';

import { UnauthorizedException } from '@nestjs/common';
import { formatException } from './utils';

export function prismaKnownRequestErrors(
error: Prisma.PrismaClientKnownRequestError,
) {
Expand All @@ -15,7 +18,7 @@ export function prismaKnownRequestErrors(

export function unknownError(error: unknown) {
// ! remover quando adicionar um logger
if (!isProductionEnv) {
if (constants().NODE_ENV != 'production') {
console.info('unknownError', error);
}

Expand All @@ -29,3 +32,21 @@ export function handleErrors(error: unknown) {

unknownError(error);
}

export function handleJWTErrors(error: Error) {
let message = 'Credenciais inválidas';

if (error.message == 'jwt expired') {
message = 'Sua chave de acesso expirou.';
} else if (error.message == 'jwt must be provided') {
message = 'Uma chave de acesso precisa ser informada.';
} else if (error.message == 'jwt malformed') {
message = 'Sua chave de acesso tem um formato errado.';
} else if (error.message == 'jwt not active') {
message = 'Sua chave de acesso ainda não está ativa.';
} else if (error.message.includes('jwt subject invalid.')) {
message = 'Sua chave de acesso não foi criada para este uso.';
}

throw new UnauthorizedException(formatException(message, 'authorization'));
}
12 changes: 12 additions & 0 deletions src/guard/authorization.decorator.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
import { SetMetadata } from '@nestjs/common';
import { Roles } from '@prisma/client';
import { iAuthTokenSubject } from 'src/modules/account/account.entity';

export type iAuthMetadata = {
type: iAuthTokenSubject;
role: Roles;
};

export const RequestToken = (authMetadata: iAuthMetadata) => {
return SetMetadata('authMetadata', authMetadata);
};
71 changes: 71 additions & 0 deletions src/guard/authorization.guard.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
import {
Injectable,
CanActivate,
ExecutionContext,
InternalServerErrorException,
UnauthorizedException,
} from '@nestjs/common';
import { Reflector } from '@nestjs/core';
import { ConfigService } from '@nestjs/config';
import { JwtService } from '@nestjs/jwt';
import { Request } from 'express';
import { formatException } from 'src/globals/utils';
import { iAuthMetadata } from './authorization.decorator';
import { handleJWTErrors } from 'src/globals/errors';

@Injectable()
export class AuthorizationGuard implements CanActivate {
private secret: string;

constructor(
private reflector: Reflector,
private configService: ConfigService,
private jwtService: JwtService,
) {
this.secret = this.configService.get<string>('SECRET_JWT');
}

private extractTokenFromHeader(request: Request): string | undefined {
const [type, token] = request.headers.authorization?.split(' ') || [];
return type === 'Bearer' ? token : undefined;
}

async canActivate(context: ExecutionContext): Promise<boolean> {
const authMetadata = this.reflector.get<iAuthMetadata>(
'authMetadata',
context.getHandler(),
);

if (!authMetadata || !authMetadata.type || !authMetadata.role) {
throw new InternalServerErrorException(
formatException('O método CanActivate precisa ser configurado.'),
);
}

const request = context.switchToHttp().getRequest();
const token = this.extractTokenFromHeader(request);

if (!token) {
throw new UnauthorizedException(
formatException('Uma chave de acesso precisa ser informada.'),
);
}

const validity = await this.jwtService
.verifyAsync(token, {
secret: this.secret,
subject: authMetadata.type,
})
.then((response) => response)
.catch((error) => handleJWTErrors(error));

if (validity) {
request['session.payload'] = validity;
return true;
}

throw new InternalServerErrorException(
formatException('O método CanActivate precisa ser verificado.'),
);
}
}
2 changes: 2 additions & 0 deletions src/guard/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
export * from './authorization.decorator';
export * from './authorization.guard';
10 changes: 10 additions & 0 deletions src/lib/jwt.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
import { ConfigService } from '@nestjs/config';
import { JwtModule } from '@nestjs/jwt';

export default JwtModule.registerAsync({
inject: [ConfigService],
useFactory: async (configService: ConfigService) => ({
secret: configService.get<string>('SECRET_JWT'),
global: true,
}),
});
7 changes: 5 additions & 2 deletions src/main.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import { NestFactory } from '@nestjs/core';
import { ConfigService } from '@nestjs/config';
import { SwaggerModule } from '@nestjs/swagger';
import helmet from 'helmet';

Expand All @@ -9,6 +10,8 @@ import { AppModule } from './app.module';

async function bootstrap() {
const app = await NestFactory.create(AppModule);
const configService = app.get(ConfigService);
const port = configService.get<string | number>('PORT');

app.use(helmet());
app.enableCors(corsOptionsConfig);
Expand All @@ -17,8 +20,8 @@ async function bootstrap() {
const document = SwaggerModule.createDocument(app, swaggerDocumentConfig);
SwaggerModule.setup('', app, document);

await app.listen(process.env.PORT, () => {
console.info(`[ONN] Port: ${process.env.PORT}`);
await app.listen(port, () => {
console.info(`[ONN] Port: ${port}`);
});
}

Expand Down
24 changes: 16 additions & 8 deletions src/modules/account/account.controller.ts
Original file line number Diff line number Diff line change
@@ -1,23 +1,29 @@
import { Controller, Post, Body, HttpCode, Put } from '@nestjs/common';
import { ConfigService } from '@nestjs/config';
import { ApiResponse, ApiTags } from '@nestjs/swagger';
import { MailService } from '../mail/mail.service';
import { AccountService } from './account.service';
import { responses } from 'src/globals/responses/docs';
import { RecoveryControllerOutput } from './account.entity';
import {
CreateAccountDto,
LoginDto,
ResetPasswordDto,
SetPasswordDto,
} from './account.dto';
import { AccountService } from './account.service';
import { responses } from 'src/globals/responses/docs';
import { isProductionEnv } from 'src/globals/constants';
import { RecoveryControllerOutput } from './account.entity';

@Controller('/auth')
export class AccountController {
private isProdEnv: boolean;

constructor(
private mailService: MailService,
private accountService: AccountService,
) {}
private configService: ConfigService,
) {
const env = this.configService.get<string>('NODE_ENV');
this.isProdEnv = env === 'production';
}

@Post('/register')
@ApiTags('Authentication')
Expand All @@ -42,11 +48,13 @@ export class AccountController {
@ApiResponse(responses.unauthorized)
@ApiResponse(responses.internalError)
async login(@Body() { email, password }: LoginDto) {
const { accessToken } = await this.accountService.login(email, password);
const { token, refresh } = await this.accountService.login(email, password);

return {
message: 'Acesso permitido',
data: {
accessToken,
accessToken: token,
refreshToken: refresh,
},
};
}
Expand All @@ -72,7 +80,7 @@ export class AccountController {
userName: created.fullname,
});

if (!isProductionEnv) {
if (!this.isProdEnv) {
response.recoverLink = created.url;
}

Expand Down
13 changes: 8 additions & 5 deletions src/modules/account/account.entity.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,8 @@ import {
ResetPasswordInfo,
} from '@prisma/client';

export type iAuthTokenSubject = 'access' | 'refresh' | 'recovery';

export type NewAccountRepositoryInput = {
credential: Omit<Credential, 'id' | 'createdAt' | 'updatedAt'>;
parentProfile: Pick<ParentProfile, 'fullname'>;
Expand All @@ -16,11 +18,12 @@ export type RecoveryControllerOutput = {
message: string;
};

export type GetCredentialIdByEmailOutput = {
id: Credential['id'];
fullname: ParentProfile['fullname'];
password: string;
} | void;
export type GetCredentialIdByEmailOutput = Pick<
Credential,
'id' | 'password'
> & {
parentProfile: Pick<ParentProfile, 'id' | 'fullname'>;
};

export type getCredentialIdByRecoveryTokenInput = {
hashedToken: string;
Expand Down
10 changes: 2 additions & 8 deletions src/modules/account/account.module.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,16 +4,10 @@ import { MailModule } from '../mail/mail.module';
import { AccountController } from './account.controller';
import { AccountService } from './account.service';
import { AccountRepository } from './account.repository';
import { JwtModule } from '@nestjs/jwt';
import jwtModule from 'src/lib/jwt';

@Module({
imports: [
PrismaModule,
MailModule,
JwtModule.register({
secret: process.env.SECRET_JWT,
}),
],
imports: [jwtModule, PrismaModule, MailModule],
controllers: [AccountController],
providers: [AccountService, AccountRepository],
})
Expand Down
13 changes: 3 additions & 10 deletions src/modules/account/account.repository.ts
Original file line number Diff line number Diff line change
Expand Up @@ -48,7 +48,7 @@ export class AccountRepository {

async getCredentialIdByEmail(
hashedEmail: string,
): Promise<GetCredentialIdByEmailOutput> {
): Promise<GetCredentialIdByEmailOutput | void> {
const response = await this.prisma.credential
.findUnique({
where: {
Expand All @@ -59,21 +59,14 @@ export class AccountRepository {
password: true,
parentProfile: {
select: {
id: true,
fullname: true,
},
},
},
})
.then((response) => {
if (response) {
return {
id: response.id,
password: response.password,
fullname: response.parentProfile.fullname,
};
}

return null;
return response;
})
.catch((error) => handleErrors(error));

Expand Down
Loading

0 comments on commit a090e36

Please sign in to comment.