├── .eslintrc.js ├── .gitignore ├── .prettierrc ├── README.md ├── docker-compose.yml ├── nest-cli.json ├── package.json ├── src ├── app.module.ts ├── auth │ ├── auth.controller.ts │ ├── auth.module.ts │ ├── auth.service.ts │ ├── constants │ │ └── jwt.constant.ts │ ├── decorators │ │ ├── auth.decorator.ts │ │ └── roles.decorator.ts │ ├── dto │ │ ├── login.dto.ts │ │ └── register.dto.ts │ └── guard │ │ ├── auth.guard.ts │ │ └── roles.guard.ts ├── breeds │ ├── breeds.controller.ts │ ├── breeds.module.ts │ ├── breeds.service.ts │ ├── dto │ │ ├── create-breed.dto.ts │ │ └── update-breed.dto.ts │ └── entities │ │ └── breed.entity.ts ├── cats │ ├── cats.controller.ts │ ├── cats.module.ts │ ├── cats.service.ts │ ├── dto │ │ ├── create-cat.dto.ts │ │ └── update-cat.dto.ts │ └── entities │ │ └── cat.entity.ts ├── common │ ├── decorators │ │ └── active-user.decorator.ts │ ├── enums │ │ └── rol.enum.ts │ └── interfaces │ │ └── user-active.interface.ts ├── main.ts └── users │ ├── dto │ ├── create-user.dto.ts │ └── update-user.dto.ts │ ├── entities │ └── user.entity.ts │ ├── users.controller.ts │ ├── users.module.ts │ └── users.service.ts ├── test ├── app.e2e-spec.ts └── jest-e2e.json ├── tsconfig.build.json ├── tsconfig.json └── yarn.lock /.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | parser: '@typescript-eslint/parser', 3 | parserOptions: { 4 | project: 'tsconfig.json', 5 | tsconfigRootDir: __dirname, 6 | sourceType: 'module', 7 | }, 8 | plugins: ['@typescript-eslint/eslint-plugin'], 9 | extends: [ 10 | 'plugin:@typescript-eslint/recommended', 11 | // 'plugin:prettier/recommended', 12 | ], 13 | root: true, 14 | env: { 15 | node: true, 16 | jest: true, 17 | }, 18 | ignorePatterns: ['.eslintrc.js'], 19 | rules: { 20 | '@typescript-eslint/interface-name-prefix': 'off', 21 | '@typescript-eslint/explicit-function-return-type': 'off', 22 | '@typescript-eslint/explicit-module-boundary-types': 'off', 23 | '@typescript-eslint/no-explicit-any': 'off', 24 | }, 25 | }; 26 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # compiled output 2 | /dist 3 | /node_modules 4 | mysql 5 | 6 | # Logs 7 | logs 8 | *.log 9 | npm-debug.log* 10 | pnpm-debug.log* 11 | yarn-debug.log* 12 | yarn-error.log* 13 | lerna-debug.log* 14 | 15 | # OS 16 | .DS_Store 17 | 18 | # Tests 19 | /coverage 20 | /.nyc_output 21 | 22 | # IDEs and editors 23 | /.idea 24 | .project 25 | .classpath 26 | .c9/ 27 | *.launch 28 | .settings/ 29 | *.sublime-workspace 30 | 31 | # IDE - VSCode 32 | .vscode/* 33 | !.vscode/settings.json 34 | !.vscode/tasks.json 35 | !.vscode/launch.json 36 | !.vscode/extensions.json -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "singleQuote": true, 3 | "trailingComma": "all" 4 | } -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 |

2 | Nest Logo 3 |

4 | 5 | [circleci-image]: https://img.shields.io/circleci/build/github/nestjs/nest/master?token=abc123def456 6 | [circleci-url]: https://circleci.com/gh/nestjs/nest 7 | 8 |

A progressive Node.js framework for building efficient and scalable server-side applications.

9 |

10 | NPM Version 11 | Package License 12 | NPM Downloads 13 | CircleCI 14 | Coverage 15 | Discord 16 | Backers on Open Collective 17 | Sponsors on Open Collective 18 | 19 | Support us 20 | 21 |

22 | 24 | 25 | ## Description 26 | 27 | [Nest](https://github.com/nestjs/nest) framework TypeScript starter repository. 28 | 29 | ## Installation 30 | 31 | ```bash 32 | $ yarn install 33 | ``` 34 | 35 | ## Running the app 36 | 37 | ```bash 38 | # development 39 | $ yarn run start 40 | 41 | # watch mode 42 | $ yarn run start:dev 43 | 44 | # production mode 45 | $ yarn run start:prod 46 | ``` 47 | 48 | ## Test 49 | 50 | ```bash 51 | # unit tests 52 | $ yarn run test 53 | 54 | # e2e tests 55 | $ yarn run test:e2e 56 | 57 | # test coverage 58 | $ yarn run test:cov 59 | ``` 60 | 61 | ## Support 62 | 63 | Nest is an MIT-licensed open source project. It can grow thanks to the sponsors and support by the amazing backers. If you'd like to join them, please [read more here](https://docs.nestjs.com/support). 64 | 65 | ## Stay in touch 66 | 67 | - Author - [Kamil Myśliwiec](https://kamilmysliwiec.com) 68 | - Website - [https://nestjs.com](https://nestjs.com/) 69 | - Twitter - [@nestframework](https://twitter.com/nestframework) 70 | 71 | ## License 72 | 73 | Nest is [MIT licensed](LICENSE). 74 | -------------------------------------------------------------------------------- /docker-compose.yml: -------------------------------------------------------------------------------- 1 | version: "3.8" 2 | services: 3 | mysql: 4 | image: mysql:8.0 5 | container_name: mysql_db 6 | restart: always 7 | environment: 8 | MYSQL_ROOT_PASSWORD: root 9 | MYSQL_DATABASE: db_crud 10 | MYSQL_USER: user_crud 11 | MYSQL_PASSWORD: root 12 | volumes: 13 | - ./mysql:/var/lib/mysql 14 | ports: 15 | - "3307:3306" 16 | -------------------------------------------------------------------------------- /nest-cli.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://json.schemastore.org/nest-cli", 3 | "collection": "@nestjs/schematics", 4 | "sourceRoot": "src", 5 | "compilerOptions": { 6 | "deleteOutDir": true 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "02-crud-mysql", 3 | "version": "0.0.1", 4 | "description": "", 5 | "author": "", 6 | "private": true, 7 | "license": "UNLICENSED", 8 | "scripts": { 9 | "build": "nest build", 10 | "format": "prettier --write \"src/**/*.ts\" \"test/**/*.ts\"", 11 | "start": "nest start", 12 | "start:dev": "nest start --watch", 13 | "start:debug": "nest start --debug --watch", 14 | "start:prod": "node dist/main", 15 | "lint": "eslint \"{src,apps,libs,test}/**/*.ts\" --fix", 16 | "test": "jest", 17 | "test:watch": "jest --watch", 18 | "test:cov": "jest --coverage", 19 | "test:debug": "node --inspect-brk -r tsconfig-paths/register -r ts-node/register node_modules/.bin/jest --runInBand", 20 | "test:e2e": "jest --config ./test/jest-e2e.json" 21 | }, 22 | "dependencies": { 23 | "@nestjs/common": "^10.0.0", 24 | "@nestjs/core": "^10.0.0", 25 | "@nestjs/jwt": "10.1.0", 26 | "@nestjs/mapped-types": "*", 27 | "@nestjs/platform-express": "^10.0.0", 28 | "@nestjs/typeorm": "10.0.0", 29 | "bcryptjs": "2.4.3", 30 | "class-transformer": "0.5.1", 31 | "class-validator": "0.14.0", 32 | "mysql2": "3.5.2", 33 | "reflect-metadata": "^0.1.13", 34 | "rxjs": "^7.8.1", 35 | "typeorm": "0.3.17" 36 | }, 37 | "devDependencies": { 38 | "@nestjs/cli": "^10.0.0", 39 | "@nestjs/schematics": "^10.0.0", 40 | "@nestjs/testing": "^10.0.0", 41 | "@types/express": "^4.17.17", 42 | "@types/jest": "^29.5.2", 43 | "@types/node": "^20.3.1", 44 | "@types/supertest": "^2.0.12", 45 | "@typescript-eslint/eslint-plugin": "^5.59.11", 46 | "@typescript-eslint/parser": "^5.59.11", 47 | "eslint": "^8.42.0", 48 | "eslint-config-prettier": "^8.8.0", 49 | "eslint-plugin-prettier": "^4.2.1", 50 | "jest": "^29.5.0", 51 | "prettier": "^2.8.8", 52 | "source-map-support": "^0.5.21", 53 | "supertest": "^6.3.3", 54 | "ts-jest": "^29.1.0", 55 | "ts-loader": "^9.4.3", 56 | "ts-node": "^10.9.1", 57 | "tsconfig-paths": "^4.2.0", 58 | "typescript": "^5.1.3" 59 | }, 60 | "jest": { 61 | "moduleFileExtensions": [ 62 | "js", 63 | "json", 64 | "ts" 65 | ], 66 | "rootDir": "src", 67 | "testRegex": ".*\\.spec\\.ts$", 68 | "transform": { 69 | "^.+\\.(t|j)s$": "ts-jest" 70 | }, 71 | "collectCoverageFrom": [ 72 | "**/*.(t|j)s" 73 | ], 74 | "coverageDirectory": "../coverage", 75 | "testEnvironment": "node" 76 | } 77 | } 78 | -------------------------------------------------------------------------------- /src/app.module.ts: -------------------------------------------------------------------------------- 1 | import { Module } from '@nestjs/common'; 2 | import { TypeOrmModule } from '@nestjs/typeorm'; 3 | import { BreedsModule } from './breeds/breeds.module'; 4 | import { CatsModule } from './cats/cats.module'; 5 | import { UsersModule } from './users/users.module'; 6 | import { AuthModule } from './auth/auth.module'; 7 | 8 | @Module({ 9 | imports: [ 10 | TypeOrmModule.forRoot({ 11 | type: 'mysql', 12 | host: 'localhost', 13 | port: 3307, 14 | username: 'user_crud', 15 | password: 'root', 16 | database: 'db_crud', 17 | autoLoadEntities: true, 18 | synchronize: true, 19 | }), 20 | CatsModule, 21 | BreedsModule, 22 | UsersModule, 23 | AuthModule, 24 | ], 25 | controllers: [], 26 | providers: [], 27 | }) 28 | export class AppModule {} 29 | -------------------------------------------------------------------------------- /src/auth/auth.controller.ts: -------------------------------------------------------------------------------- 1 | import { Body, Controller, Get, Post } from '@nestjs/common'; 2 | import { Request } from 'express'; 3 | import { ActiveUser } from 'src/common/decorators/active-user.decorator'; 4 | import { UserActiveInterface } from 'src/common/interfaces/user-active.interface'; 5 | import { Role } from '../common/enums/rol.enum'; 6 | import { AuthService } from './auth.service'; 7 | import { Auth } from './decorators/auth.decorator'; 8 | import { LoginDto } from './dto/login.dto'; 9 | import { RegisterDto } from './dto/register.dto'; 10 | 11 | interface RequestWithUser extends Request { 12 | user: { 13 | email: string; 14 | role: string; 15 | }; 16 | } 17 | 18 | @Controller('auth') 19 | export class AuthController { 20 | constructor(private readonly authService: AuthService) {} 21 | 22 | @Post('register') 23 | register( 24 | @Body() 25 | registerDto: RegisterDto, 26 | ) { 27 | return this.authService.register(registerDto); 28 | } 29 | 30 | @Post('login') 31 | login( 32 | @Body() 33 | loginDto: LoginDto, 34 | ) { 35 | return this.authService.login(loginDto); 36 | } 37 | 38 | @Get('profile') 39 | @Auth(Role.USER) 40 | profile(@ActiveUser() user: UserActiveInterface) { 41 | console.log(user) 42 | return this.authService.profile(user); 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /src/auth/auth.module.ts: -------------------------------------------------------------------------------- 1 | import { Module } from '@nestjs/common'; 2 | import { JwtModule } from '@nestjs/jwt'; 3 | import { UsersModule } from 'src/users/users.module'; 4 | import { AuthController } from './auth.controller'; 5 | import { AuthService } from './auth.service'; 6 | import { jwtConstants } from './constants/jwt.constant'; 7 | 8 | @Module({ 9 | imports: [ 10 | UsersModule, 11 | JwtModule.register({ 12 | global: true, 13 | secret: jwtConstants.secret, 14 | signOptions: { expiresIn: '1d' }, 15 | }), 16 | ], 17 | controllers: [AuthController], 18 | providers: [AuthService], 19 | }) 20 | export class AuthModule {} 21 | -------------------------------------------------------------------------------- /src/auth/auth.service.ts: -------------------------------------------------------------------------------- 1 | import { 2 | BadRequestException, 3 | Injectable, 4 | UnauthorizedException, 5 | } from '@nestjs/common'; 6 | import { UsersService } from 'src/users/users.service'; 7 | import { RegisterDto } from './dto/register.dto'; 8 | 9 | import { JwtService } from '@nestjs/jwt'; 10 | import * as bcryptjs from 'bcryptjs'; 11 | import { LoginDto } from './dto/login.dto'; 12 | 13 | @Injectable() 14 | export class AuthService { 15 | constructor( 16 | private readonly usersService: UsersService, 17 | private readonly jwtService: JwtService, 18 | ) {} 19 | 20 | async register({ name, email, password }: RegisterDto) { 21 | const user = await this.usersService.findOneByEmail(email); 22 | 23 | if (user) { 24 | throw new BadRequestException('User already exists'); 25 | } 26 | 27 | await this.usersService.create({ 28 | name, 29 | email, 30 | password: await bcryptjs.hash(password, 10), 31 | }); 32 | 33 | return { 34 | name, 35 | email, 36 | }; 37 | } 38 | 39 | async login({ email, password }: LoginDto) { 40 | const user = await this.usersService.findByEmailWithPassword(email); 41 | if (!user) { 42 | throw new UnauthorizedException('email is wrong'); 43 | } 44 | 45 | const isPasswordValid = await bcryptjs.compare(password, user.password); 46 | if (!isPasswordValid) { 47 | throw new UnauthorizedException('password is wrong'); 48 | } 49 | 50 | const payload = { email: user.email, role: user.role }; 51 | const token = await this.jwtService.signAsync(payload); 52 | 53 | return { 54 | token, 55 | email, 56 | }; 57 | } 58 | 59 | async profile({ email, role }: { email: string; role: string }) { 60 | return await this.usersService.findOneByEmail(email); 61 | } 62 | } 63 | -------------------------------------------------------------------------------- /src/auth/constants/jwt.constant.ts: -------------------------------------------------------------------------------- 1 | export const jwtConstants = { 2 | secret: 'no utilizar esta palabra en producción', 3 | }; 4 | -------------------------------------------------------------------------------- /src/auth/decorators/auth.decorator.ts: -------------------------------------------------------------------------------- 1 | import { applyDecorators, UseGuards } from '@nestjs/common'; 2 | import { Role } from '../../common/enums/rol.enum'; 3 | import { AuthGuard } from '../guard/auth.guard'; 4 | import { RolesGuard } from '../guard/roles.guard'; 5 | import { Roles } from './roles.decorator'; 6 | 7 | export function Auth(role: Role) { 8 | return applyDecorators(Roles(role), UseGuards(AuthGuard, RolesGuard)); 9 | } 10 | -------------------------------------------------------------------------------- /src/auth/decorators/roles.decorator.ts: -------------------------------------------------------------------------------- 1 | import { SetMetadata } from '@nestjs/common'; 2 | import { Role } from '../../common/enums/rol.enum'; 3 | 4 | export const ROLES_KEY = 'roles'; 5 | export const Roles = (role: Role) => SetMetadata(ROLES_KEY, role); 6 | -------------------------------------------------------------------------------- /src/auth/dto/login.dto.ts: -------------------------------------------------------------------------------- 1 | import { Transform } from 'class-transformer'; 2 | import { IsEmail, IsString, MinLength } from 'class-validator'; 3 | 4 | export class LoginDto { 5 | @IsEmail() 6 | email: string; 7 | 8 | @Transform(({ value }) => value.trim()) 9 | @IsString() 10 | @MinLength(6) 11 | password: string; 12 | } 13 | -------------------------------------------------------------------------------- /src/auth/dto/register.dto.ts: -------------------------------------------------------------------------------- 1 | import { Transform } from 'class-transformer'; 2 | import { IsEmail, IsString, MinLength } from 'class-validator'; 3 | 4 | export class RegisterDto { 5 | @Transform(({ value }) => value.trim()) 6 | @IsString() 7 | @MinLength(1) 8 | name: string; 9 | 10 | @IsEmail() 11 | email: string; 12 | 13 | @Transform(({ value }) => value.trim()) 14 | @IsString() 15 | @MinLength(6) 16 | password: string; 17 | } 18 | -------------------------------------------------------------------------------- /src/auth/guard/auth.guard.ts: -------------------------------------------------------------------------------- 1 | import { 2 | CanActivate, 3 | ExecutionContext, 4 | Injectable, 5 | UnauthorizedException, 6 | } from '@nestjs/common'; 7 | import { JwtService } from '@nestjs/jwt'; 8 | import { Request } from 'express'; 9 | import { jwtConstants } from '../constants/jwt.constant'; 10 | 11 | @Injectable() 12 | export class AuthGuard implements CanActivate { 13 | constructor(private readonly jwtService: JwtService) {} 14 | 15 | async canActivate(context: ExecutionContext): Promise { 16 | const request = context.switchToHttp().getRequest(); 17 | 18 | const token = this.extractTokenFromHeader(request); 19 | if (!token) { 20 | throw new UnauthorizedException(); 21 | } 22 | 23 | try { 24 | const payload = await this.jwtService.verifyAsync(token, { 25 | secret: jwtConstants.secret, 26 | }); 27 | request.user = payload; 28 | } catch { 29 | throw new UnauthorizedException(); 30 | } 31 | 32 | return true; 33 | } 34 | 35 | private extractTokenFromHeader(request: Request): string | undefined { 36 | const [type, token] = request.headers.authorization?.split(' ') ?? []; 37 | return type === 'Bearer' ? token : undefined; 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /src/auth/guard/roles.guard.ts: -------------------------------------------------------------------------------- 1 | import { CanActivate, ExecutionContext, Injectable } from '@nestjs/common'; 2 | import { Reflector } from '@nestjs/core'; 3 | import { Role } from '../../common/enums/rol.enum'; 4 | import { ROLES_KEY } from '../decorators/roles.decorator'; 5 | 6 | @Injectable() 7 | export class RolesGuard implements CanActivate { 8 | constructor(private readonly reflector: Reflector) {} 9 | 10 | canActivate(context: ExecutionContext): boolean { 11 | const role = this.reflector.getAllAndOverride(ROLES_KEY, [ 12 | context.getHandler(), 13 | context.getClass(), 14 | ]); 15 | 16 | if (!role) { 17 | return true; 18 | } 19 | 20 | const { user } = context.switchToHttp().getRequest(); 21 | 22 | if(user.role === Role.ADMIN) { 23 | return true; 24 | } 25 | 26 | return role === user.role; 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /src/breeds/breeds.controller.ts: -------------------------------------------------------------------------------- 1 | import { 2 | Body, 3 | Controller, 4 | Delete, 5 | Get, 6 | Param, 7 | Patch, 8 | Post, 9 | } from '@nestjs/common'; 10 | import { Auth } from 'src/auth/decorators/auth.decorator'; 11 | import { Role } from 'src/common/enums/rol.enum'; 12 | import { BreedsService } from './breeds.service'; 13 | import { CreateBreedDto } from './dto/create-breed.dto'; 14 | import { UpdateBreedDto } from './dto/update-breed.dto'; 15 | 16 | @Auth(Role.ADMIN) 17 | @Controller('breeds') 18 | export class BreedsController { 19 | constructor(private readonly breedsService: BreedsService) {} 20 | 21 | @Post() 22 | create(@Body() createBreedDto: CreateBreedDto) { 23 | return this.breedsService.create(createBreedDto); 24 | } 25 | 26 | @Get() 27 | findAll() { 28 | return this.breedsService.findAll(); 29 | } 30 | 31 | @Get(':id') 32 | findOne(@Param('id') id: number) { 33 | return this.breedsService.findOne(id); 34 | } 35 | 36 | @Patch(':id') 37 | update(@Param('id') id: number, @Body() updateBreedDto: UpdateBreedDto) { 38 | return this.breedsService.update(id, updateBreedDto); 39 | } 40 | 41 | @Delete(':id') 42 | remove(@Param('id') id: number) { 43 | return this.breedsService.remove(id); 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /src/breeds/breeds.module.ts: -------------------------------------------------------------------------------- 1 | import { Module } from '@nestjs/common'; 2 | import { TypeOrmModule } from '@nestjs/typeorm'; 3 | import { BreedsController } from './breeds.controller'; 4 | import { BreedsService } from './breeds.service'; 5 | import { Breed } from './entities/breed.entity'; 6 | 7 | @Module({ 8 | imports: [TypeOrmModule.forFeature([Breed])], 9 | controllers: [BreedsController], 10 | providers: [BreedsService], 11 | exports: [TypeOrmModule], 12 | }) 13 | export class BreedsModule {} 14 | -------------------------------------------------------------------------------- /src/breeds/breeds.service.ts: -------------------------------------------------------------------------------- 1 | import { Injectable } from '@nestjs/common'; 2 | import { InjectRepository } from '@nestjs/typeorm'; 3 | import { Repository } from 'typeorm'; 4 | import { CreateBreedDto } from './dto/create-breed.dto'; 5 | import { UpdateBreedDto } from './dto/update-breed.dto'; 6 | import { Breed } from './entities/breed.entity'; 7 | 8 | @Injectable() 9 | export class BreedsService { 10 | constructor( 11 | @InjectRepository(Breed) 12 | private readonly breedRepository: Repository, 13 | ) {} 14 | 15 | async create(createBreedDto: CreateBreedDto) { 16 | return await this.breedRepository.save(createBreedDto); 17 | } 18 | 19 | async findAll() { 20 | return await this.breedRepository.find(); 21 | } 22 | 23 | async findOne(id: number) { 24 | return `This action returns a #${id} breed`; 25 | } 26 | 27 | async update(id: number, updateBreedDto: UpdateBreedDto) { 28 | return `This action updates a #${id} breed`; 29 | } 30 | 31 | async remove(id: number) { 32 | return `This action removes a #${id} breed`; 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /src/breeds/dto/create-breed.dto.ts: -------------------------------------------------------------------------------- 1 | import { IsString, MinLength } from 'class-validator'; 2 | 3 | export class CreateBreedDto { 4 | @IsString() 5 | @MinLength(3) 6 | name: string; 7 | } 8 | -------------------------------------------------------------------------------- /src/breeds/dto/update-breed.dto.ts: -------------------------------------------------------------------------------- 1 | import { PartialType } from '@nestjs/mapped-types'; 2 | import { CreateBreedDto } from './create-breed.dto'; 3 | 4 | export class UpdateBreedDto extends PartialType(CreateBreedDto) {} 5 | -------------------------------------------------------------------------------- /src/breeds/entities/breed.entity.ts: -------------------------------------------------------------------------------- 1 | import { Cat } from 'src/cats/entities/cat.entity'; 2 | import { Column, Entity, OneToMany } from 'typeorm'; 3 | 4 | @Entity() 5 | export class Breed { 6 | @Column({ primary: true, generated: true }) 7 | id: number; 8 | 9 | @Column({ length: 500 }) 10 | name: string; 11 | 12 | @OneToMany(() => Cat, (cat) => cat.breed) 13 | cats: Cat[]; 14 | } 15 | -------------------------------------------------------------------------------- /src/cats/cats.controller.ts: -------------------------------------------------------------------------------- 1 | import { 2 | Body, 3 | Controller, 4 | Delete, 5 | Get, 6 | Param, 7 | Patch, 8 | Post, 9 | } from '@nestjs/common'; 10 | import { Auth } from 'src/auth/decorators/auth.decorator'; 11 | import { ActiveUser } from 'src/common/decorators/active-user.decorator'; 12 | import { Role } from 'src/common/enums/rol.enum'; 13 | import { UserActiveInterface } from 'src/common/interfaces/user-active.interface'; 14 | import { CatsService } from './cats.service'; 15 | import { CreateCatDto } from './dto/create-cat.dto'; 16 | import { UpdateCatDto } from './dto/update-cat.dto'; 17 | 18 | @Auth(Role.USER) 19 | @Controller('cats') 20 | export class CatsController { 21 | constructor(private readonly catsService: CatsService) {} 22 | 23 | @Post() 24 | create(@Body() createCatDto: CreateCatDto, @ActiveUser() user: UserActiveInterface) { 25 | return this.catsService.create(createCatDto, user); 26 | } 27 | 28 | @Get() 29 | findAll(@ActiveUser() user: UserActiveInterface) { 30 | return this.catsService.findAll(user); 31 | } 32 | 33 | @Get(':id') 34 | findOne(@Param('id') id: number, @ActiveUser() user: UserActiveInterface) { 35 | return this.catsService.findOne(id, user); 36 | } 37 | 38 | @Patch(':id') 39 | update(@Param('id') id: number, @Body() updateCatDto: UpdateCatDto, @ActiveUser() user: UserActiveInterface) { 40 | return this.catsService.update(id, updateCatDto, user); 41 | } 42 | 43 | @Delete(':id') 44 | remove(@Param('id') id: number, @ActiveUser() user: UserActiveInterface) { 45 | return this.catsService.remove(id, user); 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /src/cats/cats.module.ts: -------------------------------------------------------------------------------- 1 | import { Module } from '@nestjs/common'; 2 | import { TypeOrmModule } from '@nestjs/typeorm'; 3 | import { BreedsModule } from 'src/breeds/breeds.module'; 4 | import { BreedsService } from 'src/breeds/breeds.service'; 5 | import { CatsController } from './cats.controller'; 6 | import { CatsService } from './cats.service'; 7 | import { Cat } from './entities/cat.entity'; 8 | 9 | @Module({ 10 | imports: [TypeOrmModule.forFeature([Cat]), BreedsModule], 11 | controllers: [CatsController], 12 | providers: [CatsService, BreedsService], 13 | }) 14 | export class CatsModule {} 15 | -------------------------------------------------------------------------------- /src/cats/cats.service.ts: -------------------------------------------------------------------------------- 1 | import { BadRequestException, Injectable, UnauthorizedException } from '@nestjs/common'; 2 | import { InjectRepository } from '@nestjs/typeorm'; 3 | import { Repository } from 'typeorm'; 4 | import { Breed } from '../breeds/entities/breed.entity'; 5 | import { Role } from '../common/enums/rol.enum'; 6 | import { UserActiveInterface } from '../common/interfaces/user-active.interface'; 7 | import { CreateCatDto } from './dto/create-cat.dto'; 8 | import { UpdateCatDto } from './dto/update-cat.dto'; 9 | import { Cat } from './entities/cat.entity'; 10 | 11 | @Injectable() 12 | export class CatsService { 13 | constructor( 14 | @InjectRepository(Cat) 15 | private readonly catRepository: Repository, 16 | @InjectRepository(Breed) 17 | private readonly breedRepository: Repository, 18 | ) {} 19 | 20 | async create(createCatDto: CreateCatDto, user: UserActiveInterface) { 21 | const breed = await this.validateBreed(createCatDto.breed); 22 | return await this.catRepository.save({ 23 | ...createCatDto, 24 | breed: breed, 25 | userEmail: user.email, 26 | }); 27 | } 28 | 29 | async findAll(user: UserActiveInterface) { 30 | if(user.role === Role.ADMIN) { 31 | return await this.catRepository.find(); 32 | } 33 | return await this.catRepository.find({ 34 | where: { userEmail: user.email }, 35 | }); 36 | } 37 | 38 | async findOne(id: number, user: UserActiveInterface) { 39 | const cat = await this.catRepository.findOneBy({ id }); 40 | if (!cat) { 41 | throw new BadRequestException('Cat not found'); 42 | } 43 | this.validateOwnership(cat, user); 44 | return cat; 45 | } 46 | 47 | async update(id: number, updateCatDto: UpdateCatDto, user: UserActiveInterface) { 48 | await this.findOne(id, user ); 49 | return await this.catRepository.update(id, { 50 | ...updateCatDto, 51 | breed: updateCatDto.breed ? await this.validateBreed(updateCatDto.breed) : undefined, 52 | userEmail: user.email, 53 | }) 54 | } 55 | 56 | async remove(id: number, user: UserActiveInterface) { 57 | await this.findOne(id, user ); 58 | return await this.catRepository.softDelete({ id }); // se le pasa el id 59 | } 60 | 61 | private validateOwnership(cat: Cat, user: UserActiveInterface) { 62 | if (user.role !== Role.ADMIN && cat.userEmail !== user.email) { 63 | throw new UnauthorizedException(); 64 | } 65 | } 66 | 67 | private async validateBreed(breed: string) { 68 | const breedEntity = await this.breedRepository.findOneBy({ name: breed }); 69 | 70 | if (!breedEntity) { 71 | throw new BadRequestException('Breed not found'); 72 | } 73 | 74 | return breedEntity; 75 | } 76 | } 77 | -------------------------------------------------------------------------------- /src/cats/dto/create-cat.dto.ts: -------------------------------------------------------------------------------- 1 | import { 2 | IsInt, 3 | IsOptional, 4 | IsPositive, 5 | IsString, 6 | MinLength, 7 | } from 'class-validator'; 8 | 9 | export class CreateCatDto { 10 | @IsString() 11 | @MinLength(1) 12 | name: string; 13 | 14 | @IsInt() 15 | @IsPositive() 16 | age: number; 17 | 18 | @IsString() 19 | @IsOptional() 20 | breed?: string; 21 | } 22 | -------------------------------------------------------------------------------- /src/cats/dto/update-cat.dto.ts: -------------------------------------------------------------------------------- 1 | import { 2 | IsInt, 3 | IsOptional, 4 | IsPositive, 5 | IsString, 6 | MinLength, 7 | } from 'class-validator'; 8 | 9 | export class UpdateCatDto { 10 | @IsString() 11 | @MinLength(1) 12 | @IsOptional() 13 | name?: string; 14 | 15 | @IsInt() 16 | @IsPositive() 17 | @IsOptional() 18 | age?: number; 19 | 20 | @IsString() 21 | @IsOptional() 22 | breed?: string; 23 | } 24 | -------------------------------------------------------------------------------- /src/cats/entities/cat.entity.ts: -------------------------------------------------------------------------------- 1 | import { Column, DeleteDateColumn, Entity, JoinColumn, ManyToOne } from 'typeorm'; 2 | import { Breed } from '../../breeds/entities/breed.entity'; 3 | import { User } from '../../users/entities/user.entity'; 4 | 5 | @Entity() 6 | export class Cat { 7 | // @PrimaryGeneratedColumn() 8 | @Column({ primary: true, generated: true }) 9 | id: number; 10 | 11 | @Column() 12 | name: string; 13 | 14 | @Column() 15 | age: number; 16 | 17 | @DeleteDateColumn() 18 | deletedAt: Date; 19 | 20 | @ManyToOne(() => Breed, (breed) => breed.id, { 21 | eager: true, // para que traiga las raza al hacer un findOne 22 | }) 23 | breed: Breed; 24 | 25 | @ManyToOne(() => User) 26 | @JoinColumn({ name: 'userEmail', referencedColumnName: 'email', }) 27 | user: User; 28 | 29 | @Column() 30 | userEmail: string; 31 | 32 | } 33 | -------------------------------------------------------------------------------- /src/common/decorators/active-user.decorator.ts: -------------------------------------------------------------------------------- 1 | import { ExecutionContext, createParamDecorator } from '@nestjs/common'; 2 | 3 | export const ActiveUser = createParamDecorator( 4 | (data: unknown, ctx: ExecutionContext) => { 5 | const request = ctx.switchToHttp().getRequest(); 6 | return request.user 7 | } 8 | ) -------------------------------------------------------------------------------- /src/common/enums/rol.enum.ts: -------------------------------------------------------------------------------- 1 | export enum Role { 2 | USER = 'user', 3 | ADMIN = 'admin', 4 | } 5 | -------------------------------------------------------------------------------- /src/common/interfaces/user-active.interface.ts: -------------------------------------------------------------------------------- 1 | export interface UserActiveInterface { 2 | email: string; 3 | role: string; 4 | } -------------------------------------------------------------------------------- /src/main.ts: -------------------------------------------------------------------------------- 1 | import { ValidationPipe } from '@nestjs/common'; 2 | import { NestFactory } from '@nestjs/core'; 3 | import { AppModule } from './app.module'; 4 | 5 | async function bootstrap() { 6 | const app = await NestFactory.create(AppModule); 7 | 8 | app.setGlobalPrefix('api/v1'); 9 | 10 | app.useGlobalPipes( 11 | new ValidationPipe({ 12 | whitelist: true, 13 | forbidNonWhitelisted: true, 14 | transform: true, 15 | }), 16 | ); 17 | 18 | await app.listen(3000); 19 | } 20 | bootstrap(); 21 | -------------------------------------------------------------------------------- /src/users/dto/create-user.dto.ts: -------------------------------------------------------------------------------- 1 | export class CreateUserDto { 2 | email: string; 3 | password: string; 4 | name?: string; 5 | } 6 | -------------------------------------------------------------------------------- /src/users/dto/update-user.dto.ts: -------------------------------------------------------------------------------- 1 | import { PartialType } from '@nestjs/mapped-types'; 2 | import { CreateUserDto } from './create-user.dto'; 3 | 4 | export class UpdateUserDto extends PartialType(CreateUserDto) {} 5 | -------------------------------------------------------------------------------- /src/users/entities/user.entity.ts: -------------------------------------------------------------------------------- 1 | import { 2 | Column, 3 | DeleteDateColumn, 4 | Entity, 5 | PrimaryGeneratedColumn, 6 | } from 'typeorm'; 7 | import { Role } from '../../common/enums/rol.enum'; 8 | 9 | @Entity() 10 | export class User { 11 | @PrimaryGeneratedColumn() 12 | id: number; 13 | 14 | @Column() 15 | name: string; 16 | 17 | @Column({ unique: true, nullable: false }) 18 | email: string; 19 | 20 | @Column({ nullable: false, select: false }) 21 | password: string; 22 | 23 | @Column({ type: 'enum', default: Role.USER, enum: Role }) 24 | role: Role; 25 | 26 | @DeleteDateColumn() 27 | deletedAt: Date; 28 | } 29 | -------------------------------------------------------------------------------- /src/users/users.controller.ts: -------------------------------------------------------------------------------- 1 | import { Body, Controller, Delete, Get, Param, Patch, Post } from '@nestjs/common'; 2 | import { Auth } from 'src/auth/decorators/auth.decorator'; 3 | import { Role } from 'src/common/enums/rol.enum'; 4 | import { CreateUserDto } from './dto/create-user.dto'; 5 | import { UpdateUserDto } from './dto/update-user.dto'; 6 | import { UsersService } from './users.service'; 7 | 8 | @Auth(Role.ADMIN) 9 | @Controller('users') 10 | export class UsersController { 11 | constructor(private readonly usersService: UsersService) {} 12 | 13 | @Post() 14 | create(@Body() createUserDto: CreateUserDto) { 15 | return this.usersService.create(createUserDto); 16 | } 17 | 18 | @Get() 19 | findAll() { 20 | return this.usersService.findAll(); 21 | } 22 | 23 | @Get(':id') 24 | findOne(@Param('id') id: string) { 25 | return this.usersService.findOne(+id); 26 | } 27 | 28 | @Patch(':id') 29 | update(@Param('id') id: string, @Body() updateUserDto: UpdateUserDto) { 30 | return this.usersService.update(+id, updateUserDto); 31 | } 32 | 33 | @Delete(':id') 34 | remove(@Param('id') id: string) { 35 | return this.usersService.remove(+id); 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /src/users/users.module.ts: -------------------------------------------------------------------------------- 1 | import { Module } from '@nestjs/common'; 2 | import { TypeOrmModule } from '@nestjs/typeorm'; 3 | import { User } from './entities/user.entity'; 4 | import { UsersController } from './users.controller'; 5 | import { UsersService } from './users.service'; 6 | 7 | @Module({ 8 | imports: [TypeOrmModule.forFeature([User])], 9 | controllers: [UsersController], 10 | providers: [UsersService], 11 | exports: [UsersService], 12 | }) 13 | export class UsersModule {} 14 | -------------------------------------------------------------------------------- /src/users/users.service.ts: -------------------------------------------------------------------------------- 1 | import { Injectable } from '@nestjs/common'; 2 | import { InjectRepository } from '@nestjs/typeorm'; 3 | import { Repository } from 'typeorm'; 4 | import { CreateUserDto } from './dto/create-user.dto'; 5 | import { UpdateUserDto } from './dto/update-user.dto'; 6 | import { User } from './entities/user.entity'; 7 | 8 | @Injectable() 9 | export class UsersService { 10 | constructor( 11 | @InjectRepository(User) 12 | private readonly userRepository: Repository, 13 | ) {} 14 | 15 | create(createUserDto: CreateUserDto) { 16 | return this.userRepository.save(createUserDto); 17 | } 18 | 19 | findOneByEmail(email: string) { 20 | return this.userRepository.findOneBy({ email }); 21 | } 22 | 23 | findByEmailWithPassword(email: string) { 24 | return this.userRepository.findOne({ 25 | where: { email }, 26 | select: ['id', 'name', 'email', 'password', 'role'], 27 | }); 28 | } 29 | 30 | findAll() { 31 | return this.userRepository.find(); 32 | } 33 | 34 | findOne(id: number) { 35 | return `This action returns a #${id} user`; 36 | } 37 | 38 | update(id: number, updateUserDto: UpdateUserDto) { 39 | return `This action updates a #${id} user`; 40 | } 41 | 42 | remove(id: number) { 43 | return `This action removes a #${id} user`; 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /test/app.e2e-spec.ts: -------------------------------------------------------------------------------- 1 | import { Test, TestingModule } from '@nestjs/testing'; 2 | import { INestApplication } from '@nestjs/common'; 3 | import * as request from 'supertest'; 4 | import { AppModule } from './../src/app.module'; 5 | 6 | describe('AppController (e2e)', () => { 7 | let app: INestApplication; 8 | 9 | beforeEach(async () => { 10 | const moduleFixture: TestingModule = await Test.createTestingModule({ 11 | imports: [AppModule], 12 | }).compile(); 13 | 14 | app = moduleFixture.createNestApplication(); 15 | await app.init(); 16 | }); 17 | 18 | it('/ (GET)', () => { 19 | return request(app.getHttpServer()) 20 | .get('/') 21 | .expect(200) 22 | .expect('Hello World!'); 23 | }); 24 | }); 25 | -------------------------------------------------------------------------------- /test/jest-e2e.json: -------------------------------------------------------------------------------- 1 | { 2 | "moduleFileExtensions": ["js", "json", "ts"], 3 | "rootDir": ".", 4 | "testEnvironment": "node", 5 | "testRegex": ".e2e-spec.ts$", 6 | "transform": { 7 | "^.+\\.(t|j)s$": "ts-jest" 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /tsconfig.build.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./tsconfig.json", 3 | "exclude": ["node_modules", "test", "dist", "**/*spec.ts"] 4 | } 5 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "module": "commonjs", 4 | "declaration": true, 5 | "removeComments": true, 6 | "emitDecoratorMetadata": true, 7 | "experimentalDecorators": true, 8 | "allowSyntheticDefaultImports": true, 9 | "target": "ES2021", 10 | "sourceMap": true, 11 | "outDir": "./dist", 12 | "baseUrl": "./", 13 | "incremental": true, 14 | "skipLibCheck": true, 15 | "strictNullChecks": false, 16 | "noImplicitAny": false, 17 | "strictBindCallApply": false, 18 | "forceConsistentCasingInFileNames": false, 19 | "noFallthroughCasesInSwitch": false 20 | } 21 | } 22 | --------------------------------------------------------------------------------