Commit 98213cd6 authored by Guilherme Matheus Dornelles's avatar Guilherme Matheus Dornelles
Browse files

Merge branch 'feat/auth' into 'develop'

Autenticação e Autorização

See merge request !10
parents 97c5b813 9f8096dc
Showing with 805 additions and 58 deletions
+805 -58
This diff is collapsed.
import { Body, Controller, HttpCode, HttpStatus, Post } from '@nestjs/common'
import { AuthService } from './service'
import { ApiTags } from '@nestjs/swagger'
import { LoginDto } from './dto/AuthDto'
@Controller('auth')
@ApiTags('authorization')
export class AuthController {
constructor(private authService: AuthService) {}
@HttpCode(HttpStatus.OK)
@Post('/login')
signIn(@Body() signInDto: LoginDto) {
return this.authService.signIn(signInDto.email, signInDto.password)
}
}
import { ApiProperty, PickType } from '@nestjs/swagger'
import { Expose } from 'class-transformer'
import { IsString } from 'class-validator'
import { UserCreateDto } from '../../core/users/dto/UserDto'
export interface AuthDto {
userId: number
accountId: number
iat: number
exp: number
role: string
accountType: string
}
export class LoginDto {
@ApiProperty({ example: 'admin@mail.com' }) @Expose() @IsString() email: string
@ApiProperty({ example: '12345678' }) @Expose() @IsString() password: string
}
export class RegisterDto extends PickType(UserCreateDto, ['email', 'password']) {}
export class AuthResponseDto {
@ApiProperty() @Expose() @IsString() token: string
}
import { CanActivate, ExecutionContext, Injectable, UnauthorizedException } from '@nestjs/common'
import { Reflector } from '@nestjs/core'
import { Request } from 'express'
import { AuthService } from './service'
@Injectable()
export class AuthGuard implements CanActivate {
constructor(
private reflector: Reflector,
private authService: AuthService
) {}
async canActivate(context: ExecutionContext): Promise<boolean> {
const needAuth = this.reflector.get<boolean>('need_auth', context.getHandler())
if (!needAuth) return true
const request = context.switchToHttp().getRequest<Request>()
const { authorization } = request.headers
if (!authorization) throw new UnauthorizedException()
const authDto = this.authService.getPayloadFromToken(authorization as string)
if (!authDto) throw new UnauthorizedException()
return true
}
}
import { Module } from '@nestjs/common'
import { AuthService } from './service'
import { AuthController } from './controller'
import { TypeOrmModule } from '@nestjs/typeorm'
import { User } from '../../models/User.entity'
import { UserService } from '../core/users/service'
import { UserRepository } from '../core/users/repository'
import { Account } from 'src/models/Account.entity'
import { AccountRepository } from '../core/accounts/repository'
import { JwtModule } from '@nestjs/jwt'
import environment from '../../config/environment'
import { AccountService } from '../core/accounts/service'
@Module({
imports: [
TypeOrmModule.forFeature([User, Account]),
JwtModule.register({
global: true,
secret: environment.tokenSecret as string,
signOptions: { expiresIn: '3600s' }
})
],
providers: [
AuthService,
UserRepository,
UserService,
AccountRepository,
AccountRepository,
AccountService
],
controllers: [AuthController]
})
export class AuthModule {}
import { Injectable, CanActivate, ExecutionContext } from '@nestjs/common'
import { Reflector } from '@nestjs/core'
import { DEFAULT_KEYS } from 'src/config/keys'
import { UserRole } from 'src/models/enum/UsersEnum'
import { AuthService } from './service'
@Injectable()
export class RolesGuard implements CanActivate {
constructor(
private reflector: Reflector,
private authService: AuthService
) {}
canActivate(context: ExecutionContext): boolean {
const { ROLE, ACCOUNT_TYPE } = DEFAULT_KEYS
const requiredRoles = this.reflector.getAllAndOverride<UserRole[]>(ROLE, [
context.getHandler(),
context.getClass()
])
const requiredAccountType = this.reflector.getAllAndOverride<UserRole[]>(ACCOUNT_TYPE, [
context.getHandler(),
context.getClass()
])
const { authorization } = context.switchToHttp().getRequest().headers
let isRoleValid = true
if (requiredRoles) {
const { role: userRole } = this.authService.getPayloadFromToken(authorization)
isRoleValid = requiredRoles.some((role) => userRole?.includes(role))
}
let isAccountTypeValid = true
if (requiredAccountType) {
const { accountType } = this.authService.getPayloadFromToken(authorization)
isAccountTypeValid = requiredAccountType.some((type) => accountType?.includes(type))
}
return isRoleValid && isAccountTypeValid
}
}
import {
ForbiddenException,
Injectable,
NotFoundException,
UnauthorizedException
} from '@nestjs/common'
import { UserService } from '../core/users/service'
import { JwtService } from '@nestjs/jwt'
import environment from '../../config/environment'
import { AuthDto } from './dto/AuthDto'
import { AccountService } from '../core/accounts/service'
import { comparePassword } from '../../utils/password'
@Injectable()
export class AuthService {
constructor(
private usersService: UserService,
private accountsService: AccountService,
private jwtService: JwtService
) {}
public async signIn(email: string, rawPassword: string) {
const user = await this.usersService.findAll({ email: email })
if (!user.length) throw new NotFoundException('Email not registered')
const isValid = await comparePassword(rawPassword, user[0].password)
if (!isValid) {
throw new ForbiddenException('User or password invalid')
}
const account = await this.accountsService.getById(user[0].accountId)
return this.createToken(user[0]['id'], user[0]['accountId'], user[0]['role'], account['type'])
}
public getPayloadFromToken(authorization: string) {
try {
const { tokenSecret } = environment
return this.jwtService.verify(authorization.split('Bearer ')[1], tokenSecret) as AuthDto
} catch {
throw new UnauthorizedException()
}
}
private async createToken(userId: number, accountId: number, role: string, accountType: string) {
const payload = { userId: userId, accountId: accountId, role: role, accountType: accountType }
return { accessToken: await this.jwtService.signAsync(payload) }
}
}
......@@ -12,13 +12,17 @@ import {
import { ApiResponse, ApiTags } from '@nestjs/swagger'
import { AccountService } from './service'
import { AccountCreateDto, AccountItemDto, AccountUpdateDto } from './dto/AccountDto'
import { Authenticated } from 'src/decorators/authenticated'
import { AccountTypes } from 'src/decorators/roles'
import { AccountType } from 'src/models/enum/AccountsEnum'
@Controller('core/accounts')
@ApiTags('accounts')
export class AccountController {
constructor(private readonly accountService: AccountService) {}
@Get('/:accountId')
@Authenticated()
@AccountTypes(AccountType.SUPER)
@ApiResponse({ type: AccountItemDto })
async getById(@Param('accountId', ParseIntPipe) accountId: number) {
const account = await this.accountService.getById(accountId)
......@@ -27,6 +31,8 @@ export class AccountController {
}
@Post('/')
@Authenticated()
@AccountTypes(AccountType.SUPER)
@ApiResponse({ type: AccountItemDto })
async create(@Body() account: AccountCreateDto) {
const createdAccount = await this.accountService.create(account)
......@@ -35,6 +41,8 @@ export class AccountController {
}
@Patch('/:accountId')
@Authenticated()
@AccountTypes(AccountType.SUPER)
@ApiResponse({ type: AccountUpdateDto })
async update(
@Param('accountId', ParseIntPipe) accountId: number,
......@@ -45,6 +53,8 @@ export class AccountController {
}
@Delete('/:accountId')
@Authenticated()
@AccountTypes(AccountType.SUPER)
@ApiResponse({ type: AccountItemDto })
async remove(@Param('accountId', ParseIntPipe) accountId: number) {
return this.accountService.remove(accountId)
......
import { ApiProperty, PickType } from '@nestjs/swagger'
import { Expose, Type } from '@nestjs/class-transformer'
import { IsBoolean, IsEnum, IsNumber, IsOptional, IsString } from '@nestjs/class-validator'
import { Expose, Type } from 'class-transformer'
import { IsBoolean, IsEnum, IsNumber, IsOptional, IsString } from 'class-validator'
import { AccountType } from '../../../../models/enum/AccountsEnum'
export class AccountBaseDto {
......@@ -26,5 +26,5 @@ export class AccountItemDto extends PickType(AccountBaseDto, [
'createdAt'
]) {}
export class UserAccountDto extends PickType(AccountBaseDto, ['id']) {}
export class UserAccountDto extends PickType(AccountBaseDto, ['id', 'type']) {}
export class AreaAccountDto extends PickType(AccountBaseDto, ['id']) {}
......@@ -2,6 +2,10 @@ import { Controller, Get, Query, Param, ParseIntPipe, Post, Body, Patch } from '
import { AreaService } from './service'
import { ApiResponse, ApiTags } from '@nestjs/swagger'
import { AreaBaseDto, AreaCreateDto, AreaItemDto, AreaUpdateDto } from './dto/AreaDto'
import { ACCOUNTID } from 'src/decorators/accountId'
import { Roles } from 'src/decorators/roles'
import { Authenticated } from 'src/decorators/authenticated'
import { UserRole } from 'src/models/enum/UsersEnum'
@Controller('core/areas')
@ApiTags('areas')
......@@ -9,30 +13,36 @@ export class AreaController {
constructor(private readonly areaService: AreaService) {}
@Get('/')
@Authenticated()
@ApiResponse({ type: AreaItemDto })
async getAll(@Query() payload: Partial<AreaBaseDto>) {
const accountId = 1
async getAll(@ACCOUNTID() accountId: number, @Query() payload: Partial<AreaBaseDto>) {
return await this.areaService.findAll(accountId, payload)
}
@Get('/:areaId')
@Authenticated()
@ApiResponse({ type: AreaItemDto })
async getById(@Param('areaId', ParseIntPipe) areaId: number) {
const accountId = 1
async getById(@ACCOUNTID() accountId: number, @Param('areaId', ParseIntPipe) areaId: number) {
return await this.areaService.findOne(accountId, areaId)
}
@Post('/')
@Authenticated()
@Roles(UserRole.ADMIN)
@ApiResponse({ type: AreaItemDto })
async create(@Body() payload: AreaCreateDto[]) {
const accountId = 1
async create(@ACCOUNTID() accountId: number, @Body() payload: AreaCreateDto[]) {
return await this.areaService.create(accountId, payload)
}
@Patch('/:areaId')
@Authenticated()
@Roles(UserRole.ADMIN)
@ApiResponse({ type: AreaItemDto })
async update(@Param('areaId', ParseIntPipe) areaId: number, @Body() payload: AreaUpdateDto) {
const accountId = 1
async update(
@ACCOUNTID() accountId: number,
@Param('areaId', ParseIntPipe) areaId: number,
@Body() payload: AreaUpdateDto
) {
return await this.areaService.update(accountId, areaId, payload)
}
}
import { ApiProperty } from '@nestjs/swagger'
import { PickType } from '@nestjs/mapped-types'
import { Expose } from '@nestjs/class-transformer'
import { IsNumber, IsString } from '@nestjs/class-validator'
import { Expose } from 'class-transformer'
import { IsNumber, IsString } from 'class-validator'
export class AreaBaseDto {
@ApiProperty() @Expose() @IsNumber() id: number
......
......@@ -2,8 +2,9 @@ import { Module } from '@nestjs/common'
import { AccountModule } from './accounts/module'
import { UserModule } from './users/module'
import { AreaModule } from './areas/module'
import { AuthModule } from '../auth/module'
@Module({
imports: [AccountModule, UserModule, AreaModule]
imports: [AccountModule, UserModule, AreaModule, AuthModule]
})
export class CoreModule {}
......@@ -11,6 +11,10 @@ import {
import { ApiTags, ApiResponse } from '@nestjs/swagger'
import { serialize } from 'src/utils/serializer'
import { ParseIntPipe } from '@nestjs/common/pipes'
import { ACCOUNTID } from 'src/decorators/accountId'
import { Authenticated } from 'src/decorators/authenticated'
import { Roles } from 'src/decorators/roles'
import { UserRole } from 'src/models/enum/UsersEnum'
@Controller('core/users')
@ApiTags('users')
......@@ -18,14 +22,15 @@ export class UserController {
constructor(private readonly userService: UserService) {}
@Get('/:userId')
@Authenticated()
@ApiResponse({ type: UserItemDto })
async getById(@Param('userId', ParseIntPipe) userId: number) {
const accountId = 1
async getById(@ACCOUNTID() accountId: number, @Param('userId', ParseIntPipe) userId: number) {
const user = await this.userService.findOne(accountId, userId)
return serialize(UserItemDto, user)
}
@Get('/')
@Authenticated()
@ApiResponse({ type: UserItemDto })
async getAll(@Query() payload: Partial<UserSearchDto>) {
const users = await this.userService.findAll(payload)
......@@ -33,6 +38,8 @@ export class UserController {
}
@Post('/')
@Authenticated()
@Roles(UserRole.ADMIN)
@ApiResponse({ type: UserItemDto })
async create(@Body() user: UserCreateDto) {
const createdUser = await this.userService.create(user)
......@@ -40,6 +47,8 @@ export class UserController {
}
@Post('/batch')
@Authenticated()
@Roles(UserRole.ADMIN)
@ApiResponse({ type: UserBatchCreateDto })
async createBatch(@Body() users: UserBatchCreateDto) {
const createdUser = await this.userService.createBatch(users)
......@@ -47,20 +56,23 @@ export class UserController {
}
@Patch('/:userId')
@Authenticated()
@Roles(UserRole.ADMIN)
@ApiResponse({ type: UserItemDto })
async update(
@ACCOUNTID() accountId: number,
@Body() userPayload: Partial<UserUpdateDto>,
@Param('userId', ParseIntPipe) userId: number
) {
const accountId = 1
const updatedUser = await this.userService.update(accountId, userId, userPayload)
return serialize(UserItemDto, updatedUser)
}
@Delete('/:userId')
@Authenticated()
@Roles(UserRole.ADMIN)
@ApiResponse({ type: UserItemDto })
async remove(@Param('userId', ParseIntPipe) userId: number) {
const accountId = 1
async remove(@ACCOUNTID() accountId: number, @Param('userId', ParseIntPipe) userId: number) {
return this.userService.remove(accountId, userId)
}
}
import { ApiProperty, IntersectionType, PickType } from '@nestjs/swagger'
import { Expose } from '@nestjs/class-transformer'
import { Expose, Type } from 'class-transformer'
import {
IsBoolean,
IsNumber,
......@@ -8,9 +8,8 @@ import {
IsEmail,
MinLength,
IsDate
} from '@nestjs/class-validator'
} from 'class-validator'
import { UserRole } from 'src/models/enum/UsersEnum'
import { Type } from 'class-transformer'
export class UserBaseDto {
@ApiProperty() @Expose() @IsNumber() id: number
......
......@@ -14,6 +14,7 @@ import {
UserUpdateDto
} from './dto/UserDto'
import { AccountRepository } from '../accounts/repository'
import { hashPassword } from '../../../utils/password'
@Injectable()
export class UserService {
......@@ -45,6 +46,8 @@ export class UserService {
const existingAccount = (await this.accountRepository.findBy({ id: payload.accountId })).length
payload.password = await hashPassword(payload.password)
if (existingAccount) {
return await this.userRepository.create(payload)
} else {
......
import * as dotenv from 'dotenv'
import { serialize } from 'src/utils/serializer'
import { Expose, Type } from 'class-transformer'
import { IsNumber, IsOptional, IsString, validateSync } from 'class-validator'
import { JwtVerifyOptions } from '@nestjs/jwt'
dotenv.config()
interface DatabaseConfig {
host: string
port: number
username: string
password: string
}
interface Config {
port: number
database: DatabaseConfig
tokenSecret: JwtVerifyOptions
}
class EnvironmentConfig {
@Expose() @IsOptional() @IsNumber() @Type(() => Number) PORT: string
@Expose() @IsString({ message: 'Database host is required.' }) DATABASE_HOST: string
@Expose() @IsOptional() @IsNumber() @Type(() => Number) DATABASE_PORT: string
@Expose() @IsString({ message: 'Database username is required.' }) DATABASE_USERNAME: string
@Expose() @IsString({ message: 'Database password is required.' }) DATABASE_PASSWORD: string
@Expose() @IsString({ message: 'Token secret is required.' }) TOKEN_SECRET: JwtVerifyOptions
}
const envVars = serialize(EnvironmentConfig, process.env)
const validationErrors = validateSync(envVars)
if (validationErrors.length > 0)
throw new Error(validationErrors.map((x) => x.toString()).join('\n'))
const config: Config = {
port: parseInt(envVars.PORT) || 5000,
database: {
host: envVars.DATABASE_HOST || 'localhost',
port: parseInt(envVars.DATABASE_PORT) || 5432,
username: envVars.DATABASE_USERNAME,
password: envVars.DATABASE_PASSWORD
},
tokenSecret: envVars.TOKEN_SECRET
}
export default config
export const DEFAULT_KEYS = {
ROLE: 'role',
ACCOUNT_TYPE: 'accountType'
}
import { createParamDecorator, ExecutionContext } from '@nestjs/common'
import { JwtService } from '@nestjs/jwt'
export const ACCOUNTID = createParamDecorator((_data: unknown, ctx: ExecutionContext) => {
const jwtService = new JwtService()
const request = ctx.switchToHttp().getRequest()
return jwtService.decode(request.headers.authorization.split('Bearer ')[1]).accountId
})
import { SetMetadata } from '@nestjs/common'
import { applyDecorators } from '@nestjs/common/decorators/core/apply-decorators'
import { ApiBearerAuth } from '@nestjs/swagger'
export const Authenticated = () => applyDecorators(SetMetadata('need_auth', true), ApiBearerAuth())
Markdown is supported
0% or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment