Skip to content

Commit

Permalink
user roles done
Browse files Browse the repository at this point in the history
  • Loading branch information
ruslanguns committed Aug 30, 2020
1 parent 79410c3 commit d8ac8a1
Show file tree
Hide file tree
Showing 19 changed files with 288 additions and 45 deletions.
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@
"class-transformer": "^0.3.1",
"class-validator": "^0.12.2",
"mysql": "^2.18.1",
"nest-access-control": "^2.0.2",
"passport": "^0.4.1",
"passport-jwt": "^4.0.0",
"passport-local": "^1.0.0",
Expand Down
11 changes: 7 additions & 4 deletions src/app.module.ts
Original file line number Diff line number Diff line change
@@ -1,17 +1,18 @@
import { Module } from '@nestjs/common';
import { ConfigModule, ConfigService } from '@nestjs/config'
import { TypeOrmModule } from '@nestjs/typeorm';
import { AccessControlModule } from 'nest-access-control';

import { AppController } from './app.controller';
import { AppService } from './app.service';
import { PostModule } from './post/post.module';
import { TypeOrmModule } from '@nestjs/typeorm';
import { UserModule } from './user/user.module';
import { DATABASE_HOST, DATABASE_PORT, DATABASE_USERNAME, DATABASE_PASSWORD, DATABASE_NAME } from './config/constants';
import { AuthModule } from './auth/auth.module';
import { DATABASE_HOST, DATABASE_PORT, DATABASE_USERNAME, DATABASE_PASSWORD, DATABASE_NAME } from './config/constants';
import { roles } from './app.roles';

@Module({
imports: [
PostModule,
TypeOrmModule.forRootAsync({
inject: [ConfigService],
useFactory: (config: ConfigService) => ({
Expand All @@ -32,8 +33,10 @@ import { AuthModule } from './auth/auth.module';
isGlobal: true,
envFilePath: '.env'
}),
UserModule,
AccessControlModule.forRoles(roles),
AuthModule,
UserModule,
PostModule,
],
controllers: [AppController],
providers: [AppService],
Expand Down
29 changes: 29 additions & 0 deletions src/app.roles.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
import { RolesBuilder } from "nest-access-control";

export enum AppRoles {
AUTHOR = 'AUTHOR',
ADMIN = 'ADMIN'
}

export enum AppResource {
USER = 'USER',
POST = 'POST'
}

export const roles: RolesBuilder = new RolesBuilder();

roles
// AUTHOR ROLES
.grant(AppRoles.AUTHOR)
.updateOwn([AppResource.USER])
.deleteOwn([AppResource.USER])
.createOwn([AppResource.POST])
.updateOwn([AppResource.POST])
.deleteOwn([AppResource.POST])
// ADMIN ROLES
.grant(AppRoles.ADMIN)
.extend(AppRoles.AUTHOR)
.createAny([AppResource.USER])
.updateAny([AppResource.POST, AppResource.USER])
.deleteAny([AppResource.POST, AppResource.USER])

8 changes: 5 additions & 3 deletions src/auth/auth.controller.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,10 @@
import { Controller, Post, Get, UseGuards } from '@nestjs/common';
import { LocalAuthGuard, JwtAuthGuard } from './guards';
import { Controller, Post, Get, UseGuards, Body } from '@nestjs/common';
import { LocalAuthGuard } from './guards';
import { User, Auth } from 'src/common/decorators';
import { User as UserEntity } from 'src/user/entities';
import { AuthService } from './auth.service';
import { ApiTags, ApiBearerAuth } from '@nestjs/swagger';
import { ApiTags } from '@nestjs/swagger';
import { LoginDto } from './dtos/login.dto';

@ApiTags('Auth routes')
@Controller('auth')
Expand All @@ -16,6 +17,7 @@ export class AuthController {
@UseGuards(LocalAuthGuard)
@Post('login')
async login(
@Body() loginDto: LoginDto,
@User() user: UserEntity
) {
const data = await this.authService.login(user);
Expand Down
6 changes: 6 additions & 0 deletions src/auth/dtos/login.dto.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@


export class LoginDto {
email: string;
password: string;
}
6 changes: 4 additions & 2 deletions src/common/decorators/auth.decorator.ts
Original file line number Diff line number Diff line change
@@ -1,11 +1,13 @@
import { applyDecorators, UseGuards } from "@nestjs/common";
import { ApiBearerAuth } from "@nestjs/swagger";
import { JwtAuthGuard } from "src/auth/guards";
import { ACGuard, Role, UseRoles } from "nest-access-control";


export function Auth() {
export function Auth(...roles: Role[]) {
return applyDecorators(
UseGuards(JwtAuthGuard),
UseGuards(JwtAuthGuard, ACGuard),
UseRoles(...roles),
ApiBearerAuth()
)
}
4 changes: 3 additions & 1 deletion src/config/constants.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,4 +4,6 @@ export const DATABASE_HOST = 'DATABASE_HOST';
export const DATABASE_PORT = 'DATABASE_PORT';
export const DATABASE_USERNAME = 'DATABASE_USERNAME';
export const DATABASE_PASSWORD = 'DATABASE_PASSWORD';
export const DATABASE_NAME = 'DATABASE_NAME';
export const DATABASE_NAME = 'DATABASE_NAME';
export const DEFAULT_USER_EMAIL = 'DEFAULT_USER_EMAIL';
export const DEFAULT_USER_PASSWORD = 'DEFAULT_USER_PASSWORD';
23 changes: 23 additions & 0 deletions src/config/default-user.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
import { getRepository } from 'typeorm'
import { ConfigService } from '@nestjs/config'
import { DEFAULT_USER_EMAIL, DEFAULT_USER_PASSWORD } from './constants'
import { User } from 'src/user/entities'

export const setDefaultUser = async (config: ConfigService) => {
const userRepository = getRepository<User>(User)

const defaultUser = await userRepository
.createQueryBuilder()
.where('email = :email', { email: config.get<string>('DEFAULT_USER_EMAIL') })
.getOne()

if (!defaultUser) {
const adminUser = userRepository.create({
email: config.get<string>(DEFAULT_USER_EMAIL),
password: config.get<string>(DEFAULT_USER_PASSWORD),
roles: ['ADMIN']
})

return await userRepository.save(adminUser)
}
}
2 changes: 2 additions & 0 deletions src/main.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import { AppModule } from './app.module';
import { initSwagger } from './app.swagger';
import { ConfigService } from '@nestjs/config';
import { SERVER_PORT } from './config/constants';
import { setDefaultUser } from './config/default-user';

async function bootstrap() {
const app = await NestFactory.create(AppModule);
Expand All @@ -13,6 +14,7 @@ async function bootstrap() {
const port = parseInt(config.get<string>(SERVER_PORT), 10) || 3000;

initSwagger(app);
setDefaultUser(config);

app.useGlobalPipes(
new ValidationPipe({
Expand Down
7 changes: 7 additions & 0 deletions src/post/entities/post.entity.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,10 @@ import {
PrimaryGeneratedColumn,
Column,
CreateDateColumn,
ManyToOne,
JoinColumn,
} from 'typeorm';
import { User } from 'src/user/entities';

@Entity('posts')
export class Post {
Expand Down Expand Up @@ -33,4 +36,8 @@ export class Post {

@CreateDateColumn({ type: 'timestamp' })
createdAt: Date;

@ManyToOne(_ => User, (user) => user.posts, { eager: true })
@JoinColumn({ name: 'author' })
author: User;
}
78 changes: 69 additions & 9 deletions src/post/post.controller.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,15 +9,22 @@ import {
ParseIntPipe,
} from '@nestjs/common';
import { ApiTags } from '@nestjs/swagger';

import { InjectRolesBuilder, RolesBuilder } from 'nest-access-control';
import { AppResource } from 'src/app.roles';
import { PostService } from './post.service';
import { CreatePostDto, EditPostDto } from './dtos';
import { User, Auth } from 'src/common/decorators';
import { User as UserEntity } from 'src/user/entities';

@ApiTags('Posts')
@Controller('post')
export class PostController {
constructor(private readonly postService: PostService) {}

constructor(
private readonly postService: PostService,
@InjectRolesBuilder()
private readonly roleBuilder: RolesBuilder,
) {}

@Get()
async getMany() {
const data = await this.postService.getMany();
Expand All @@ -30,21 +37,74 @@ export class PostController {
return { data };
}

@Auth({
resource: AppResource.POST,
action: 'create',
possession: 'own'
})
@Post()
async createPost(@Body() dto: CreatePostDto) {
const data = await this.postService.createOne(dto);
async createPost(
@Body() dto: CreatePostDto,
@User() author: UserEntity
) {
const data = await this.postService.createOne(dto, author);
return { message: 'Post created', data };
}

@Auth({
resource: AppResource.POST,
action: 'update',
possession: 'own'
})
@Put(':id')
async editOne(@Param('id') id: number, @Body() dto: EditPostDto) {
const data = await this.postService.editOne(id, dto);
async editOne(
@Param('id') id: number, @Body() dto: EditPostDto,
@User() author: UserEntity
) {

let data;

if (
this.roleBuilder
.can(author.roles)
.updateAny(AppResource.POST)
.granted
) {

// Puede editar cualquier POST...
data = await this.postService.editOne(id, dto);

} else {

// Puede editar solo los propios...
data = await this.postService.editOne(id, dto, author);
}

return { message: 'Post edited', data };
}

@Auth({
resource: AppResource.POST,
action: 'delete',
possession: 'own'
})
@Delete(':id')
async deleteOne(@Param('id') id: number) {
const data = await this.postService.deleteOne(id);
async deleteOne(
@Param('id') id: number,
@User() author: UserEntity
) {

let data;

if (this.roleBuilder
.can(author.roles)
.deleteAny(AppResource.POST)
.granted
) {
data = await this.postService.deleteOne(id);
} else {
data = await this.postService.deleteOne(id, author);
}
return { message: 'Post deleted', data };
}
}
24 changes: 12 additions & 12 deletions src/post/post.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import { Repository } from 'typeorm';

import { Post } from './entities';
import { CreatePostDto, EditPostDto } from './dtos';
import { User } from 'src/user/entities';

@Injectable()
export class PostService {
Expand All @@ -16,27 +17,26 @@ export class PostService {
return await this.postRepository.find();
}

async getById(id: number) {
const post = await this.postRepository.findOne(id);
if (!post) throw new NotFoundException('Post does not exist');
async getById(id: number, author?: User) {
const post = await this.postRepository.findOne(id)
.then(p => !author ? p : !!p && author.id === p.author.id ? p : null)
if (!post) throw new NotFoundException('Post does not exist or unauthorized');
return post;
}

async createOne(dto: CreatePostDto) {
const post = this.postRepository.create(dto);
async createOne(dto: CreatePostDto, author: User) {
const post = this.postRepository.create({...dto, author});
return await this.postRepository.save(post);
}

async editOne(id: number, dto: EditPostDto) {
const post = await this.postRepository.findOne(id);

if (!post) throw new NotFoundException('Post does not exist');

async editOne(id: number, dto: EditPostDto, author?: User) {
const post = await this.getById(id, author);
const editedPost = Object.assign(post, dto);
return await this.postRepository.save(editedPost);
}

async deleteOne(id: number) {
return await this.postRepository.delete(id);
async deleteOne(id: number, author?: User) {
const post = await this.getById(id, author);
return await this.postRepository.remove(post);
}
}
11 changes: 10 additions & 1 deletion src/user/dtos/create-user.dto.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,6 @@
import { IsString, IsEmail, MinLength, MaxLength, IsOptional } from "class-validator";
import { IsString, IsEmail, MinLength, MaxLength, IsOptional, IsArray, IsEnum } from "class-validator";
import { AppRoles } from "src/app.roles";
import { EnumToString } from "src/common/helpers/enumToString";

export class CreateUserDto {
@IsOptional()
Expand All @@ -19,4 +21,11 @@ export class CreateUserDto {
@MaxLength(128)
password: string;

@IsArray()
@IsEnum(AppRoles, {
each: true,
message: `must be a valid role value, ${ EnumToString(AppRoles)}`
})
roles: string[];

}
3 changes: 2 additions & 1 deletion src/user/dtos/index.ts
Original file line number Diff line number Diff line change
@@ -1,2 +1,3 @@
export * from './create-user.dto';
export * from './edit-user.dto';
export * from './edit-user.dto';
export * from './user-registration.dto';
4 changes: 4 additions & 0 deletions src/user/dtos/user-registration.dto.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
import { OmitType } from "@nestjs/mapped-types";
import { CreateUserDto } from "./create-user.dto";

export class UserRegistrationDto extends OmitType(CreateUserDto, ['roles'] as const) {}
Loading

0 comments on commit d8ac8a1

Please sign in to comment.