Skip to content

Commit

Permalink
[add] role validation
Browse files Browse the repository at this point in the history
  • Loading branch information
Excalibur authored and Excalibur committed Sep 10, 2022
1 parent d6c9e53 commit eb909f7
Show file tree
Hide file tree
Showing 30 changed files with 274 additions and 65 deletions.
23 changes: 23 additions & 0 deletions .vscode/launch.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
{
"version": "0.2.0",
"configurations": [
{
"type": "node",
"request": "launch",
"name": "Debug Nest Framework",
"args": ["${workspaceFolder}/src/main.ts"],
"runtimeArgs": [
"--nolazy",
"-r",
"ts-node/register",
"-r",
"tsconfig-paths/register"
],
"sourceMaps": true,
"envFile": "${workspaceFolder}/.env",
"cwd": "${workspaceRoot}",
"console": "integratedTerminal",
"autoAttachChildProcesses": true
}
]
}
10 changes: 9 additions & 1 deletion src/auth/auth.controller.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,11 @@
import { Controller, Post, Body } from '@nestjs/common';
import { Controller, Post, Body, Get } from '@nestjs/common';
import { ApiTags } from '@nestjs/swagger';

import { AuthService } from './auth.service';
import { CreateUserDto } from 'src/user/dto';
import { LoginAuthDto } from './dto';
import { Auth, GetUser } from './decorators';
import { User } from 'src/user/entities/user.entity';

@ApiTags('auth')
@Controller('auth')
Expand All @@ -19,4 +21,10 @@ export class AuthController {
loginAuth(@Body() loginAuthDto: LoginAuthDto) {
return this.authService.loginAuth(loginAuthDto);
}

@Get('check-status')
@Auth()
checkAuthStatus(@GetUser() user: User) {
return this.authService.checkAuthStatus(user);
}
}
18 changes: 13 additions & 5 deletions src/auth/auth.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ import * as bcrypt from 'bcrypt';

import { CreateUserDto } from 'src/user/dto';
import { User } from 'src/user/entities/user.entity';
import { MessageHandler } from 'src/utils/enums/message.handler';
import { MessageHandler } from 'src/shared/enums';
import { LoginAuthDto } from './dto';
import { JwtPayload } from './interfaces/jwt-payload.interface';

Expand All @@ -30,7 +30,6 @@ export class AuthService {
async createAuth(createUserDto: CreateUserDto) {
try {
const { password, ...userData } = createUserDto;

const user = this.userRepository.create({
...userData,
password: bcrypt.hashSync(password, 10),
Expand All @@ -40,7 +39,7 @@ export class AuthService {
delete user.password;
return {
...user,
token: this.getJwtToken({ email: user.email }),
token: this.getJwtToken({ id: user.id }),
};
} catch (error) {
if (error.code === '23505')
Expand All @@ -58,7 +57,7 @@ export class AuthService {
const { password, email } = loginUserDto;
const user = await this.userRepository.findOne({
where: { email },
select: { email: true, password: true },
select: { email: true, password: true, id: true },
});

if (!user)
Expand All @@ -67,9 +66,18 @@ export class AuthService {
if (!bcrypt.compareSync(password, user.password))
throw new UnauthorizedException(MessageHandler.UNAUTHORIZED_CREDENTIALS);

delete user.id;

return {
...user,
token: this.getJwtToken({ id: user.id }),
};
}

checkAuthStatus(user: User) {
return {
...user,
token: this.getJwtToken({ email: user.email }),
token: this.getJwtToken({ id: user.id }),
};
}

Expand Down
13 changes: 13 additions & 0 deletions src/auth/decorators/auth.decorator.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
import { applyDecorators, UseGuards } from '@nestjs/common';
import { AuthGuard } from '@nestjs/passport';

import { RoleProtected } from 'src/auth/decorators';
import { ValidRoles } from 'src/shared/enums';
import { UserRoleGuard } from '../guards/user-role.guard';

export function Auth(...roles: ValidRoles[]) {
return applyDecorators(
RoleProtected(...roles),
UseGuards(AuthGuard(), UserRoleGuard),
);
}
19 changes: 19 additions & 0 deletions src/auth/decorators/get-user.decorator.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
import {
createParamDecorator,
ExecutionContext,
InternalServerErrorException,
} from '@nestjs/common';

import { MessageHandler } from 'src/shared/enums';

export const GetUser = createParamDecorator(
(data: string, ctx: ExecutionContext) => {
const req = ctx.switchToHttp().getRequest();
const user = req.user;

if (!user)
throw new InternalServerErrorException(MessageHandler.USER_NOT_FOUND);

return !data ? user : user[data];
},
);
3 changes: 3 additions & 0 deletions src/auth/decorators/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
export { Auth } from './auth.decorator';
export { RoleProtected } from './role-protected.decorator';
export { GetUser } from './get-user.decorator';
5 changes: 5 additions & 0 deletions src/auth/decorators/role-protected.decorator.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
import { SetMetadata } from '@nestjs/common';
import { MetaRoles, ValidRoles } from 'src/shared/enums';

export const RoleProtected = (...args: ValidRoles[]) =>
SetMetadata(MetaRoles.ROLES, args);
4 changes: 2 additions & 2 deletions src/auth/dto/login-auth.dto.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,8 +7,8 @@ import {
MinLength,
} from 'class-validator';

import { validatePassword } from 'src/utils/actions/validations';
import { MessageHandler } from 'src/utils/enums/message.handler';
import { validatePassword } from 'src/shared/actions/validations';
import { MessageHandler } from 'src/shared/enums';

export class LoginAuthDto {
@ApiProperty()
Expand Down
41 changes: 41 additions & 0 deletions src/auth/guards/user-role.guard.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
import { Reflector } from '@nestjs/core';
import {
BadRequestException,
CanActivate,
ExecutionContext,
ForbiddenException,
Injectable,
} from '@nestjs/common';
import { Observable } from 'rxjs';

import { MessageHandler, MetaRoles } from 'src/shared/enums';
import { User } from 'src/user/entities/user.entity';

@Injectable()
export class UserRoleGuard implements CanActivate {
constructor(private readonly reflector: Reflector) {}
canActivate(
context: ExecutionContext,
): boolean | Promise<boolean> | Observable<boolean> {
const validRoles: string[] = this.reflector.get(
MetaRoles.ROLES,
context.getHandler(),
);

if (!validRoles) return true;
if (validRoles.length === 0) return true;

const req = context.switchToHttp().getRequest();
const user = req.user as User;

if (!user) throw new BadRequestException(MessageHandler.USER_NOT_FOUND);

for (const role of user.roles) {
if (validRoles.includes(role)) {
return true;
}
}

throw new ForbiddenException(MessageHandler.USER_INVALID_ROLE);
}
}
2 changes: 1 addition & 1 deletion src/auth/interfaces/jwt-payload.interface.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,3 @@
export interface JwtPayload {
email: string;
id: string;
}
8 changes: 4 additions & 4 deletions src/auth/strategies/jwt.strategy.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ import { InjectRepository } from '@nestjs/typeorm';

import { JwtPayload } from '../interfaces/jwt-payload.interface';
import { User } from 'src/user/entities/user.entity';
import { MessageHandler } from 'src/utils/enums/message.handler';
import { MessageHandler, ValidState } from 'src/shared/enums';

@Injectable()
export class JwtStrategy extends PassportStrategy(Strategy) {
Expand All @@ -24,12 +24,12 @@ export class JwtStrategy extends PassportStrategy(Strategy) {
}

async validate(payload: JwtPayload): Promise<User> {
const { email } = payload;
const user = await this.userRepository.findOneBy({ email });
const { id } = payload;
const user = await this.userRepository.findOneBy({ id });
if (!user)
throw new UnauthorizedException(MessageHandler.UNAUTHORIZED_TOKEN);

if (!user.active)
if (user.state !== ValidState.ACTIVE)
throw new UnauthorizedException(MessageHandler.UNAUTHORIZED_USER);

return user;
Expand Down
6 changes: 5 additions & 1 deletion src/products/entities/product.entity.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import { Column, Entity, PrimaryGeneratedColumn } from 'typeorm';
import { User } from 'src/user/entities/user.entity';
import { Column, Entity, ManyToOne, PrimaryGeneratedColumn } from 'typeorm';

@Entity()
export class Product {
Expand All @@ -25,4 +26,7 @@ export class Product {

@Column('bool', { default: true })
active: boolean;

@ManyToOne(() => User, (user) => user.product, { eager: true })
user: User;
}
7 changes: 5 additions & 2 deletions src/products/products.controller.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,20 +16,23 @@ import {

import { ProductsService } from './products.service';
import { CreateProductDto, UpdateProductDto } from 'src/products/dto';
import { Auth, GetUser } from 'src/auth/decorators';
import { User } from 'src/user/entities/user.entity';

@ApiTags('products')
@Controller('products')
export class ProductsController {
constructor(private readonly productsService: ProductsService) {}

@Post()
@Auth()
@ApiOperation({ summary: 'Create a new product' })
@ApiCreatedResponse({
description: 'The record has been successfully created.',
})
@ApiForbiddenResponse({ description: 'Forbidden.' })
create(@Body() createProductDto: CreateProductDto) {
return this.productsService.create(createProductDto);
create(@Body() createProductDto: CreateProductDto, @GetUser() user: User) {
return this.productsService.create(createProductDto, user);
}

@Get()
Expand Down
4 changes: 3 additions & 1 deletion src/products/products.module.ts
Original file line number Diff line number Diff line change
@@ -1,13 +1,15 @@
import { Module } from '@nestjs/common';
import { TypeOrmModule } from '@nestjs/typeorm';

import { AuthModule } from 'src/auth/auth.module';

import { ProductsService } from './products.service';
import { ProductsController } from './products.controller';
import { Product } from './entities/product.entity';

@Module({
controllers: [ProductsController],
providers: [ProductsService],
imports: [TypeOrmModule.forFeature([Product])],
imports: [TypeOrmModule.forFeature([Product]), AuthModule],
})
export class ProductsModule {}
7 changes: 4 additions & 3 deletions src/products/products.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,8 @@ import { Repository } from 'typeorm';

import { CreateProductDto, UpdateProductDto } from 'src/products/dto';
import { Product } from './entities/product.entity';
import { MessageHandler } from 'src/utils/enums/message.handler';
import { MessageHandler } from 'src/shared/enums';
import { User } from 'src/user/entities/user.entity';

@Injectable()
export class ProductsService {
Expand All @@ -22,9 +23,10 @@ export class ProductsService {

private readonly logger = new Logger('ProductsService');

async create(createProductDto: CreateProductDto) {
async create(createProductDto: CreateProductDto, user: User) {
try {
const product = this.productRepository.create(createProductDto);
product.user = user;
await this.productRepository.save(product);
return product;
} catch (error) {
Expand All @@ -47,7 +49,6 @@ export class ProductsService {
const product = await this.productRepository.findOneBy({ id });
if (!product)
throw new NotFoundException(`Product with id ${id} not found`);
console.log(product);

return product;
}
Expand Down
File renamed without changes.
1 change: 1 addition & 0 deletions src/shared/decorators/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export { RawHeaders } from './raw-headers.decorator';
11 changes: 11 additions & 0 deletions src/shared/decorators/raw-headers.decorator.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
import { createParamDecorator, ExecutionContext } from '@nestjs/common';

export const RawHeaders = createParamDecorator(
(data: string, ctx: ExecutionContext) => {
const req = ctx.switchToHttp().getRequest();
return req.rawHeaders;
},
);

//uso @RawHeader() rawHeaders: string[]
// best use @Headers() headers: IncomingHttpHeaders
4 changes: 4 additions & 0 deletions src/shared/enums/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
export { MessageHandler } from './message.handler';
export { MetaRoles } from './meta.roles';
export { ValidRoles } from './valid.roles';
export { ValidState } from './valid.state';
Original file line number Diff line number Diff line change
@@ -1,7 +1,17 @@
export enum MessageHandler {
PASSWORD_INVALID = 'The password must have a Uppercase, lowercase letter and a number',

UNEXPECTED_ERROR = 'Unexpected error, check server logs',

UNAUTHORIZED_CREDENTIALS = 'Email or password are not valid',
UNAUTHORIZED_TOKEN = 'Token is not valid',
UNAUTHORIZED_USER = 'User is inactive, contact support',

USERS_NOT_FOUND = 'Users not found',
USER_NOT_FOUND = 'User not found',
USER_INVALID_ROLE = 'User does not have permissions',
USER_INVALID_STATUS = 'User does not have valid status',
USER_INACTIVE = 'User is inactive',

EMAIL_ALREADY_EXIST = 'Email is all ready exist',
}
3 changes: 3 additions & 0 deletions src/shared/enums/meta.roles.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
export enum MetaRoles {
ROLES = 'roles',
}
4 changes: 4 additions & 0 deletions src/shared/enums/valid.roles.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
export enum ValidRoles {
USER = 'User',
ADMIN = 'Admin',
}
6 changes: 6 additions & 0 deletions src/shared/enums/valid.state.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
export enum ValidState {
ACTIVE = 'Active',
PREREGISTER = 'Preregister',
INACTIVE = 'Inactive',
SUSPENDED = 'Suspended',
}
Loading

0 comments on commit eb909f7

Please sign in to comment.