├── .prettierrc ├── src ├── auth │ ├── roles │ │ ├── roles.ts │ │ ├── roles.decorator.ts │ │ ├── roles.guard.spec.ts │ │ └── roles.guard.ts │ ├── errors.ts │ ├── auth.guard.spec.ts │ ├── login.dto.ts │ ├── auth.controller.ts │ ├── auth.service.spec.ts │ ├── auth.controller.spec.ts │ ├── filters │ │ └── invalid-credentials-error.filter.ts │ ├── auth.service.ts │ ├── auth.guard.ts │ └── auth.module.ts ├── app.service.ts ├── common │ ├── errors.ts │ └── filters │ │ └── not-found-error.filter.ts ├── products │ ├── admin │ │ ├── dto │ │ │ ├── update-product.dto.ts │ │ │ └── create-product.dto.ts │ │ ├── admin-products.controller.ts │ │ └── admin-products.service.ts │ ├── errors.ts │ ├── public │ │ ├── dto │ │ │ └── product-query.dto.ts │ │ ├── products.controller.ts │ │ └── products.service.ts │ ├── products.service.spec.ts │ ├── filters │ │ └── product-slug-already-exists.filter.ts │ └── products.module.ts ├── prisma │ ├── prisma.module.ts │ ├── prisma.service.ts │ └── prisma.service.spec.ts ├── app.controller.ts ├── app.module.ts ├── app.controller.spec.ts └── main.ts ├── prisma ├── database.sqlite ├── migrations │ ├── migration_lock.toml │ ├── 20241203224348_ │ │ └── migration.sql │ ├── 20241203225433_ │ │ └── migration.sql │ └── 20241203235732_ │ │ └── migration.sql └── schema.prisma ├── tsconfig.build.json ├── README.md ├── docker-compose.yaml ├── Dockerfile ├── nest-cli.json ├── test ├── jest-e2e.json └── app.e2e-spec.ts ├── .env.example ├── tsconfig.json ├── .eslintrc.js ├── .gitignore ├── api.http └── package.json /.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "singleQuote": true, 3 | "trailingComma": "all" 4 | } -------------------------------------------------------------------------------- /src/auth/roles/roles.ts: -------------------------------------------------------------------------------- 1 | export enum UserRoles { 2 | Admin = 'admin', 3 | Customer = 'customer', 4 | } 5 | -------------------------------------------------------------------------------- /prisma/database.sqlite: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/devfullcycle/live-imersao-20-nestjs-api/HEAD/prisma/database.sqlite -------------------------------------------------------------------------------- /tsconfig.build.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./tsconfig.json", 3 | "exclude": ["node_modules", "test", "dist", "**/*spec.ts"] 4 | } 5 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | No branch `auth-register` há uma implementação de registro de usuários e outras coisas mostrando melhor ainda como funciona o Nest.js 2 | -------------------------------------------------------------------------------- /docker-compose.yaml: -------------------------------------------------------------------------------- 1 | services: 2 | app: 3 | build: . 4 | ports: 5 | - 3000:3000 6 | volumes: 7 | - .:/home/node/app 8 | -------------------------------------------------------------------------------- /src/auth/errors.ts: -------------------------------------------------------------------------------- 1 | export class InvalidCredentialsError extends Error { 2 | constructor() { 3 | super('Invalid credentials'); 4 | } 5 | } 6 | -------------------------------------------------------------------------------- /prisma/migrations/migration_lock.toml: -------------------------------------------------------------------------------- 1 | # Please do not edit this file manually 2 | # It should be added in your version-control system (i.e. Git) 3 | provider = "sqlite" -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | FROM node:23.0.0-slim 2 | 3 | RUN apt update && apt install -y openssl procps 4 | 5 | USER node 6 | 7 | WORKDIR /home/node/app 8 | 9 | CMD [ "tail", "-f", "/dev/null" ] -------------------------------------------------------------------------------- /src/app.service.ts: -------------------------------------------------------------------------------- 1 | import { Injectable } from '@nestjs/common'; 2 | 3 | @Injectable() 4 | export class AppService { 5 | getHello(): string { 6 | return 'Hello World!'; 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /src/auth/roles/roles.decorator.ts: -------------------------------------------------------------------------------- 1 | import { SetMetadata } from '@nestjs/common'; 2 | import { UserRoles } from './roles'; 3 | 4 | export const Roles = (...args: UserRoles[]) => SetMetadata('roles', args); 5 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /src/auth/auth.guard.spec.ts: -------------------------------------------------------------------------------- 1 | import { AuthGuard } from './auth.guard'; 2 | 3 | describe('AuthGuard', () => { 4 | it('should be defined', () => { 5 | expect(new AuthGuard()).toBeDefined(); 6 | }); 7 | }); 8 | -------------------------------------------------------------------------------- /src/common/errors.ts: -------------------------------------------------------------------------------- 1 | export class NotFoundError extends Error { 2 | constructor(entity: string, key: string, attribute: string = 'id') { 3 | super(`${entity} with ${attribute} ${key} not found`); 4 | } 5 | } 6 | -------------------------------------------------------------------------------- /src/auth/roles/roles.guard.spec.ts: -------------------------------------------------------------------------------- 1 | import { RolesGuard } from './roles.guard'; 2 | 3 | describe('RolesGuard', () => { 4 | it('should be defined', () => { 5 | expect(new RolesGuard()).toBeDefined(); 6 | }); 7 | }); 8 | -------------------------------------------------------------------------------- /src/products/admin/dto/update-product.dto.ts: -------------------------------------------------------------------------------- 1 | import { PartialType } from '@nestjs/mapped-types'; 2 | import { CreateProductDto } from './create-product.dto'; 3 | 4 | export class UpdateProductDto extends PartialType(CreateProductDto) {} 5 | -------------------------------------------------------------------------------- /src/products/errors.ts: -------------------------------------------------------------------------------- 1 | export class ProductSlugAlreadyExistsError extends Error { 2 | constructor(slug: string) { 3 | super(`Product with slug ${slug} already exists`); 4 | this.name = 'ProductSlugAlreadyExistsError'; 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /src/auth/login.dto.ts: -------------------------------------------------------------------------------- 1 | import { IsEmail, IsNotEmpty, IsString } from 'class-validator'; 2 | 3 | export class LoginDto { 4 | @IsEmail() 5 | @IsNotEmpty() 6 | email: string; 7 | 8 | @IsString() 9 | @IsNotEmpty() 10 | password: string; 11 | } 12 | -------------------------------------------------------------------------------- /prisma/migrations/20241203224348_/migration.sql: -------------------------------------------------------------------------------- 1 | -- CreateTable 2 | CREATE TABLE "Product" ( 3 | "id" TEXT NOT NULL PRIMARY KEY, 4 | "name" TEXT NOT NULL, 5 | "slug" TEXT NOT NULL, 6 | "description" TEXT NOT NULL, 7 | "price" REAL NOT NULL 8 | ); 9 | -------------------------------------------------------------------------------- /src/prisma/prisma.module.ts: -------------------------------------------------------------------------------- 1 | import { Global, Module } from '@nestjs/common'; 2 | import { PrismaService } from './prisma.service'; 3 | 4 | @Global() 5 | @Module({ 6 | providers: [PrismaService], 7 | exports: [PrismaService], 8 | }) 9 | export class PrismaModule {} 10 | -------------------------------------------------------------------------------- /prisma/migrations/20241203225433_/migration.sql: -------------------------------------------------------------------------------- 1 | /* 2 | Warnings: 3 | 4 | - A unique constraint covering the columns `[slug]` on the table `Product` will be added. If there are existing duplicate values, this will fail. 5 | 6 | */ 7 | -- CreateIndex 8 | CREATE UNIQUE INDEX "Product_slug_key" ON "Product"("slug"); 9 | -------------------------------------------------------------------------------- /src/prisma/prisma.service.ts: -------------------------------------------------------------------------------- 1 | import { Injectable, OnModuleInit } from '@nestjs/common'; 2 | import { PrismaClient } from '@prisma/client'; 3 | 4 | @Injectable() 5 | export class PrismaService extends PrismaClient implements OnModuleInit { 6 | async onModuleInit() { 7 | await this.$connect(); 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /prisma/migrations/20241203235732_/migration.sql: -------------------------------------------------------------------------------- 1 | -- CreateTable 2 | CREATE TABLE "User" ( 3 | "id" TEXT NOT NULL PRIMARY KEY, 4 | "name" TEXT NOT NULL, 5 | "email" TEXT NOT NULL, 6 | "password" TEXT NOT NULL, 7 | "role" TEXT NOT NULL 8 | ); 9 | 10 | -- CreateIndex 11 | CREATE UNIQUE INDEX "User_email_key" ON "User"("email"); 12 | -------------------------------------------------------------------------------- /src/products/public/dto/product-query.dto.ts: -------------------------------------------------------------------------------- 1 | import { IsInt, IsOptional, IsString, Min } from 'class-validator'; 2 | 3 | export class ProductQueryDto { 4 | @IsString() 5 | @IsOptional() 6 | name: string; 7 | 8 | @Min(1) 9 | @IsInt() 10 | @IsOptional() 11 | page: number; 12 | 13 | @Min(1) 14 | @IsInt() 15 | @IsOptional() 16 | limit: number; 17 | } 18 | -------------------------------------------------------------------------------- /src/auth/auth.controller.ts: -------------------------------------------------------------------------------- 1 | import { Body, Controller, Post } from '@nestjs/common'; 2 | import { LoginDto } from './login.dto'; 3 | import { AuthService } from './auth.service'; 4 | 5 | @Controller('auth') 6 | export class AuthController { 7 | constructor(private authService: AuthService) {} 8 | 9 | @Post('login') 10 | login(@Body() dto: LoginDto) { 11 | return this.authService.login(dto); 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /src/app.controller.ts: -------------------------------------------------------------------------------- 1 | import { Controller, Get, Post } from '@nestjs/common'; 2 | import { AppService } from './app.service'; 3 | 4 | @Controller('prefixo') 5 | export class AppController { 6 | constructor(private readonly appService: AppService) {} 7 | 8 | @Get('hello') 9 | getHello(): string { 10 | return this.appService.getHello(); 11 | } 12 | 13 | @Post() 14 | xpto() { 15 | return 'POST'; 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /.env.example: -------------------------------------------------------------------------------- 1 | # Environment variables declared in this file are automatically made available to Prisma. 2 | # See the documentation for more detail: https://pris.ly/d/prisma-schema#accessing-environment-variables-from-the-schema 3 | 4 | # Prisma supports the native connection string format for PostgreSQL, MySQL, SQLite, SQL Server, MongoDB and CockroachDB. 5 | # See the documentation for all the connection string options: https://pris.ly/d/connection-strings 6 | 7 | DATABASE_URL="file:./database.sqlite" -------------------------------------------------------------------------------- /src/auth/auth.service.spec.ts: -------------------------------------------------------------------------------- 1 | import { Test, TestingModule } from '@nestjs/testing'; 2 | import { AuthService } from './auth.service'; 3 | 4 | describe('AuthService', () => { 5 | let service: AuthService; 6 | 7 | beforeEach(async () => { 8 | const module: TestingModule = await Test.createTestingModule({ 9 | providers: [AuthService], 10 | }).compile(); 11 | 12 | service = module.get(AuthService); 13 | }); 14 | 15 | it('should be defined', () => { 16 | expect(service).toBeDefined(); 17 | }); 18 | }); 19 | -------------------------------------------------------------------------------- /src/app.module.ts: -------------------------------------------------------------------------------- 1 | import { Module } from '@nestjs/common'; 2 | import { AppController } from './app.controller'; 3 | import { AppService } from './app.service'; 4 | import { ProductsModule } from './products/products.module'; 5 | import { PrismaModule } from './prisma/prisma.module'; 6 | import { AuthModule } from './auth/auth.module'; 7 | 8 | @Module({ 9 | imports: [PrismaModule, ProductsModule, AuthModule], 10 | controllers: [AppController], 11 | providers: [AppService], 12 | }) 13 | export class AppModule {} 14 | 15 | //MVC - Model View Controller 16 | -------------------------------------------------------------------------------- /src/prisma/prisma.service.spec.ts: -------------------------------------------------------------------------------- 1 | import { Test, TestingModule } from '@nestjs/testing'; 2 | import { PrismaService } from './prisma.service'; 3 | 4 | describe('PrismaService', () => { 5 | let service: PrismaService; 6 | 7 | beforeEach(async () => { 8 | const module: TestingModule = await Test.createTestingModule({ 9 | providers: [PrismaService], 10 | }).compile(); 11 | 12 | service = module.get(PrismaService); 13 | }); 14 | 15 | it('should be defined', () => { 16 | expect(service).toBeDefined(); 17 | }); 18 | }); 19 | -------------------------------------------------------------------------------- /src/auth/auth.controller.spec.ts: -------------------------------------------------------------------------------- 1 | import { Test, TestingModule } from '@nestjs/testing'; 2 | import { AuthController } from './auth.controller'; 3 | 4 | describe('AuthController', () => { 5 | let controller: AuthController; 6 | 7 | beforeEach(async () => { 8 | const module: TestingModule = await Test.createTestingModule({ 9 | controllers: [AuthController], 10 | }).compile(); 11 | 12 | controller = module.get(AuthController); 13 | }); 14 | 15 | it('should be defined', () => { 16 | expect(controller).toBeDefined(); 17 | }); 18 | }); 19 | -------------------------------------------------------------------------------- /src/common/filters/not-found-error.filter.ts: -------------------------------------------------------------------------------- 1 | import { ArgumentsHost, Catch, ExceptionFilter } from '@nestjs/common'; 2 | import { NotFoundError } from '../errors'; 3 | import { Response } from 'express'; 4 | 5 | @Catch(NotFoundError) 6 | export class NotFoundErrorFilter implements ExceptionFilter { 7 | catch(exception: NotFoundError, host: ArgumentsHost) { 8 | const ctx = host.switchToHttp(); 9 | const response = ctx.getResponse(); 10 | 11 | response.status(404).json({ 12 | statusCode: 404, 13 | message: exception.message, 14 | }); 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /src/products/products.service.spec.ts: -------------------------------------------------------------------------------- 1 | import { Test, TestingModule } from '@nestjs/testing'; 2 | import { AdminProductsService } from './admin/admin-products.service'; 3 | 4 | describe('ProductsService', () => { 5 | let service: AdminProductsService; 6 | 7 | beforeEach(async () => { 8 | const module: TestingModule = await Test.createTestingModule({ 9 | providers: [AdminProductsService], 10 | }).compile(); 11 | 12 | service = module.get(AdminProductsService); 13 | }); 14 | 15 | it('should be defined', () => { 16 | expect(service).toBeDefined(); 17 | }); 18 | }); 19 | -------------------------------------------------------------------------------- /src/auth/filters/invalid-credentials-error.filter.ts: -------------------------------------------------------------------------------- 1 | import { ArgumentsHost, Catch, ExceptionFilter } from '@nestjs/common'; 2 | import { InvalidCredentialsError } from '../errors'; 3 | import { Response } from 'express'; 4 | 5 | @Catch(InvalidCredentialsError) 6 | export class InvalidCredentialsErrorFilter implements ExceptionFilter { 7 | catch(exception: InvalidCredentialsError, host: ArgumentsHost) { 8 | const ctx = host.switchToHttp(); 9 | const response = ctx.getResponse(); 10 | 11 | response.status(401).json({ 12 | statusCode: 401, 13 | message: 'Invalid credentials', 14 | }); 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /src/products/filters/product-slug-already-exists.filter.ts: -------------------------------------------------------------------------------- 1 | import { ArgumentsHost, Catch, ExceptionFilter } from '@nestjs/common'; 2 | import { ProductSlugAlreadyExistsError } from '../errors'; 3 | import { Response } from 'express'; 4 | 5 | @Catch(ProductSlugAlreadyExistsError) 6 | export class ProductSlugAlreadyExistsErrorFilter implements ExceptionFilter { 7 | catch(exception: any, host: ArgumentsHost) { 8 | const ctx = host.switchToHttp(); 9 | const response = ctx.getResponse(); 10 | 11 | response.status(409).json({ 12 | statusCode: 409, 13 | message: exception.message, 14 | }); 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /src/products/admin/dto/create-product.dto.ts: -------------------------------------------------------------------------------- 1 | import { 2 | IsNotEmpty, 3 | IsNumber, 4 | IsString, 5 | Matches, 6 | MaxLength, 7 | Min, 8 | } from 'class-validator'; 9 | 10 | export class CreateProductDto { 11 | @MaxLength(255) 12 | @IsString() 13 | @IsNotEmpty() 14 | name: string; 15 | 16 | @Matches(/^[a-z0-9-]+$/, { 17 | message: 'slug can only contain lowercase letters, numbers and dashes', 18 | }) 19 | @IsNotEmpty() 20 | slug: string; 21 | 22 | @MaxLength(500) 23 | @IsString() 24 | @IsNotEmpty() 25 | description: string; 26 | 27 | @Min(1) 28 | @IsNumber({ maxDecimalPlaces: 2 }) 29 | @IsNotEmpty() 30 | price: number; 31 | } 32 | -------------------------------------------------------------------------------- /src/products/public/products.controller.ts: -------------------------------------------------------------------------------- 1 | import { Controller, Get, Param, Query } from '@nestjs/common'; 2 | import { ProductsService } from './products.service'; 3 | import { ProductQueryDto } from './dto/product-query.dto'; 4 | 5 | @Controller('products') 6 | export class ProductsController { 7 | constructor(private readonly productsService: ProductsService) {} 8 | 9 | @Get() 10 | findAll(@Query() queryDto: ProductQueryDto) { 11 | return this.productsService.findAll(queryDto); 12 | } 13 | 14 | @Get(':slug') 15 | findOne(@Param('slug') slug: string) { 16 | return this.productsService.findOne(slug); 17 | } 18 | } 19 | 20 | //SOLID 21 | // Single Responsibility Principle 22 | -------------------------------------------------------------------------------- /src/app.controller.spec.ts: -------------------------------------------------------------------------------- 1 | import { Test, TestingModule } from '@nestjs/testing'; 2 | import { AppController } from './app.controller'; 3 | import { AppService } from './app.service'; 4 | 5 | describe('AppController', () => { 6 | let appController: AppController; 7 | 8 | beforeEach(async () => { 9 | const app: TestingModule = await Test.createTestingModule({ 10 | controllers: [AppController], 11 | providers: [AppService], 12 | }).compile(); 13 | 14 | appController = app.get(AppController); 15 | }); 16 | 17 | describe('root', () => { 18 | it('should return "Hello World!"', () => { 19 | expect(appController.getHello()).toBe('Hello World!'); 20 | }); 21 | }); 22 | }); 23 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /.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 | -------------------------------------------------------------------------------- /src/auth/roles/roles.guard.ts: -------------------------------------------------------------------------------- 1 | import { CanActivate, ExecutionContext, Injectable } from '@nestjs/common'; 2 | import { Reflector } from '@nestjs/core'; 3 | import { Observable } from 'rxjs'; 4 | 5 | @Injectable() 6 | export class RolesGuard implements CanActivate { 7 | constructor(private reflector: Reflector) {} 8 | canActivate( 9 | context: ExecutionContext, 10 | ): boolean | Promise | Observable { 11 | const { user } = context.switchToHttp().getRequest(); 12 | const requiredRoles = this.reflector.getAllAndOverride('roles', [ 13 | context.getHandler(), 14 | context.getClass(), 15 | ]); 16 | 17 | if (!requiredRoles) { 18 | return true; 19 | } 20 | 21 | return requiredRoles.some((role) => user.role === role); 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /prisma/schema.prisma: -------------------------------------------------------------------------------- 1 | // This is your Prisma schema file, 2 | // learn more about it in the docs: https://pris.ly/d/prisma-schema 3 | 4 | // Looking for ways to speed up your queries, or scale easily with your serverless or edge functions? 5 | // Try Prisma Accelerate: https://pris.ly/cli/accelerate-init 6 | 7 | generator client { 8 | provider = "prisma-client-js" 9 | } 10 | 11 | datasource db { 12 | provider = "sqlite" 13 | url = env("DATABASE_URL") 14 | } 15 | 16 | model Product { 17 | id String @id @default(cuid()) 18 | name String 19 | slug String @unique 20 | description String 21 | price Float 22 | } 23 | 24 | model User { 25 | id String @id @default(cuid()) 26 | name String 27 | email String @unique 28 | password String 29 | role String 30 | } 31 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # compiled output 2 | /dist 3 | /node_modules 4 | /build 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 37 | 38 | # dotenv environment variable files 39 | .env 40 | .env.development.local 41 | .env.test.local 42 | .env.production.local 43 | .env.local 44 | 45 | # temp directory 46 | .temp 47 | .tmp 48 | 49 | # Runtime data 50 | pids 51 | *.pid 52 | *.seed 53 | *.pid.lock 54 | 55 | # Diagnostic reports (https://nodejs.org/api/report.html) 56 | report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json 57 | .history/ -------------------------------------------------------------------------------- /src/auth/auth.service.ts: -------------------------------------------------------------------------------- 1 | import { Injectable } from '@nestjs/common'; 2 | import { LoginDto } from './login.dto'; 3 | import { PrismaService } from '../prisma/prisma.service'; 4 | import { InvalidCredentialsError } from './errors'; 5 | import * as bcrypt from 'bcrypt'; 6 | import { JwtService } from '@nestjs/jwt'; 7 | 8 | @Injectable() 9 | export class AuthService { 10 | constructor( 11 | private prismaService: PrismaService, 12 | private jwtService: JwtService, 13 | ) {} 14 | 15 | async login(dto: LoginDto) { 16 | const user = await this.prismaService.user.findFirst({ 17 | where: { 18 | email: dto.email, 19 | }, 20 | }); 21 | 22 | if (!user || !bcrypt.compareSync(dto.password, user.password)) { 23 | throw new InvalidCredentialsError(); 24 | } 25 | 26 | const payload = { 27 | sub: user.id, 28 | role: user.role, 29 | }; 30 | 31 | return { 32 | access_token: this.jwtService.sign(payload), 33 | }; 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /src/products/public/products.service.ts: -------------------------------------------------------------------------------- 1 | import { Injectable } from '@nestjs/common'; 2 | import { PrismaService } from '../../prisma/prisma.service'; 3 | import { NotFoundError } from '../../common/errors'; 4 | 5 | @Injectable() 6 | export class ProductsService { 7 | constructor(private prismaService: PrismaService) {} 8 | 9 | findAll(dto: { name?: string; page?: number; limit?: number }) { 10 | const { name, page = 1, limit = 15 } = dto; 11 | return this.prismaService.product.findMany({ 12 | ...(name && { 13 | where: { 14 | name: { 15 | contains: name, 16 | }, 17 | }, 18 | }), 19 | skip: (page - 1) * limit, 20 | take: limit, 21 | }); 22 | } 23 | 24 | async findOne(slug: string) { 25 | const product = await this.prismaService.product.findFirst({ 26 | where: { 27 | slug, 28 | }, 29 | }); 30 | 31 | if (!product) { 32 | throw new NotFoundError('Product', slug, 'slug'); 33 | } 34 | 35 | return product; 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /src/main.ts: -------------------------------------------------------------------------------- 1 | import { NestFactory } from '@nestjs/core'; 2 | import { AppModule } from './app.module'; 3 | import { ProductSlugAlreadyExistsErrorFilter } from './products/filters/product-slug-already-exists.filter'; 4 | import { ValidationPipe } from '@nestjs/common'; 5 | import { NotFoundErrorFilter } from './common/filters/not-found-error.filter'; 6 | import { InvalidCredentialsErrorFilter } from './auth/filters/invalid-credentials-error.filter'; 7 | 8 | async function bootstrap() { 9 | const app = await NestFactory.create(AppModule, { cors: true }); 10 | 11 | app.useGlobalFilters( 12 | new ProductSlugAlreadyExistsErrorFilter(), 13 | new NotFoundErrorFilter(), 14 | new InvalidCredentialsErrorFilter(), 15 | ); //tratamento de erros 16 | app.useGlobalPipes( 17 | new ValidationPipe({ 18 | errorHttpStatusCode: 422, 19 | transform: true, 20 | transformOptions: { 21 | enableImplicitConversion: true, 22 | }, 23 | }), 24 | ); 25 | 26 | await app.listen(process.env.PORT ?? 3000); 27 | } 28 | bootstrap(); 29 | -------------------------------------------------------------------------------- /api.http: -------------------------------------------------------------------------------- 1 | ### 2 | # @name admin_jwt_login 3 | POST http://localhost:3000/auth/login 4 | Content-Type: application/json 5 | 6 | { 7 | "email": "admin@user.com", 8 | "password": "secret" 9 | } 10 | 11 | ### 12 | 13 | @jwt = {{ admin_jwt_login.response.body.access_token }} 14 | 15 | ### 16 | POST http://localhost:3000/prefixo 17 | 18 | ### 19 | GET http://localhost:3000/admin/products 20 | Authorization: Bearer {{ jwt }} 21 | 22 | ### 23 | # @name createProduct 24 | POST http://localhost:3000/admin/products 25 | Content-Type: application/json 26 | Authorization: Bearer jwt 27 | 28 | { 29 | "name": "Product 1", 30 | "slug": "product-14444", 31 | "description": "Product 1 description", 32 | "price": 9.99 33 | } 34 | 35 | ### 36 | 37 | @productId = {{ createProduct.response.body.id }} 38 | 39 | ### 40 | GET http://localhost:3000/admin/products/1 41 | 42 | ### 43 | PATCH http://localhost:3000/admin/products/cm493nlnm0000s9ecqhw6wqah 44 | Content-Type: application/json 45 | 46 | { 47 | "slug": "product-xpto" 48 | 49 | } 50 | 51 | 52 | ### 53 | DELETE http://localhost:3000/admin/products/{{productId}} 54 | 55 | 56 | ### open products 57 | 58 | GET http://localhost:3000/products?page=1 59 | 60 | ### 61 | GET http://localhost:3000/products/product-12 -------------------------------------------------------------------------------- /src/products/products.module.ts: -------------------------------------------------------------------------------- 1 | import { Module, OnModuleInit } from '@nestjs/common'; 2 | import { AdminProductsService } from './admin/admin-products.service'; 3 | import { AdminProductsController } from './admin/admin-products.controller'; 4 | import { PrismaService } from '../prisma/prisma.service'; 5 | import { ProductsController } from './public/products.controller'; 6 | import { ProductsService } from './public/products.service'; 7 | 8 | @Module({ 9 | controllers: [AdminProductsController, ProductsController], 10 | providers: [AdminProductsService, ProductsService], 11 | }) 12 | export class ProductsModule implements OnModuleInit { 13 | constructor( 14 | private prismaService: PrismaService, 15 | private productsService: AdminProductsService, 16 | ) {} 17 | 18 | async onModuleInit() { 19 | const products = new Array(10).fill(0).map((_, index) => index + 1); 20 | 21 | await this.prismaService.product.deleteMany(); 22 | 23 | for (const productIndex of products) { 24 | await this.productsService.create({ 25 | name: `Product ${productIndex}`, 26 | slug: `product-${productIndex}`, 27 | description: `Description of product ${productIndex}`, 28 | price: productIndex * 100, 29 | }); 30 | } 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /src/auth/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 | 10 | @Injectable() 11 | export class AuthGuard implements CanActivate { 12 | constructor(private jwtService: JwtService) {} 13 | 14 | async canActivate(context: ExecutionContext): Promise { 15 | const request = context.switchToHttp().getRequest(); 16 | const token = this.extractTokenFromHeader(request); 17 | console.log(token); 18 | if (!token) { 19 | throw new UnauthorizedException(); 20 | } 21 | try { 22 | const payload = await this.jwtService.verifyAsync(token, { 23 | secret: 'secret', 24 | }); 25 | // 💡 We're assigning the payload to the request object here 26 | // so that we can access it in our route handlers 27 | request['user'] = payload; 28 | } catch { 29 | throw new UnauthorizedException(); 30 | } 31 | return true; 32 | } 33 | 34 | private extractTokenFromHeader(request: Request): string | undefined { 35 | const [type, token] = request.headers.authorization?.split(' ') ?? []; 36 | return type === 'Bearer' ? token : undefined; 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /src/products/admin/admin-products.controller.ts: -------------------------------------------------------------------------------- 1 | import { 2 | Controller, 3 | Get, 4 | Post, 5 | Body, 6 | Patch, 7 | Param, 8 | Delete, 9 | HttpCode, 10 | UseGuards, 11 | } from '@nestjs/common'; 12 | import { AdminProductsService } from './admin-products.service'; 13 | import { CreateProductDto } from './dto/create-product.dto'; 14 | import { UpdateProductDto } from './dto/update-product.dto'; 15 | import { AuthGuard } from '../../auth/auth.guard'; 16 | import { Roles } from '../../auth/roles/roles.decorator'; 17 | import { UserRoles } from '../../auth/roles/roles'; 18 | import { RolesGuard } from '../../auth/roles/roles.guard'; 19 | 20 | @Roles(UserRoles.Admin) 21 | @UseGuards(AuthGuard, RolesGuard) 22 | @Controller('admin/products') 23 | export class AdminProductsController { 24 | constructor(private readonly productsService: AdminProductsService) {} 25 | 26 | @Post() 27 | create(@Body() createProductDto: CreateProductDto) { 28 | return this.productsService.create(createProductDto); 29 | } 30 | 31 | @Get() 32 | findAll() { 33 | return this.productsService.findAll(); 34 | } 35 | 36 | @Get(':id') 37 | findOne(@Param('id') id: string) { 38 | return this.productsService.findOne(id); 39 | } 40 | 41 | @Patch(':id') 42 | update(@Param('id') id: string, @Body() updateProductDto: UpdateProductDto) { 43 | return this.productsService.update(id, updateProductDto); 44 | } 45 | 46 | @HttpCode(204) 47 | @Delete(':id') 48 | remove(@Param('id') id: string) { 49 | return this.productsService.remove(id); 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /src/auth/auth.module.ts: -------------------------------------------------------------------------------- 1 | import { Module, OnModuleInit } from '@nestjs/common'; 2 | import { AuthController } from './auth.controller'; 3 | import { AuthService } from './auth.service'; 4 | import { PrismaService } from '../prisma/prisma.service'; 5 | import { UserRoles } from './roles/roles'; 6 | import * as bcrypt from 'bcrypt'; 7 | import { JwtModule } from '@nestjs/jwt'; 8 | 9 | @Module({ 10 | imports: [ 11 | JwtModule.register({ 12 | global: true, 13 | secret: 'secret', 14 | signOptions: { expiresIn: '1d' }, 15 | }), 16 | ], 17 | controllers: [AuthController], 18 | providers: [AuthService], 19 | }) 20 | export class AuthModule implements OnModuleInit { 21 | constructor(private prismaService: PrismaService) {} 22 | 23 | async onModuleInit() { 24 | const customerUser = await this.prismaService.user.findFirst({ 25 | where: { 26 | email: 'customer@user.com', 27 | }, 28 | }); 29 | 30 | if (!customerUser) { 31 | await this.prismaService.user.create({ 32 | data: { 33 | email: 'customer@user.com', 34 | name: 'Customer User', 35 | password: bcrypt.hashSync('secret', 10), 36 | role: UserRoles.Customer, 37 | }, 38 | }); 39 | } 40 | 41 | const adminUser = await this.prismaService.user.findFirst({ 42 | where: { 43 | email: 'admin@user.com', 44 | }, 45 | }); 46 | 47 | if (!adminUser) { 48 | await this.prismaService.user.create({ 49 | data: { 50 | email: 'admin@user.com', 51 | name: 'Admin User', 52 | password: bcrypt.hashSync('secret', 10), 53 | role: UserRoles.Admin, 54 | }, 55 | }); 56 | } 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "nestjs-api", 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.2.0", 26 | "@nestjs/mapped-types": "*", 27 | "@nestjs/platform-express": "^10.0.0", 28 | "@prisma/client": "^6.0.1", 29 | "@types/bcrypt": "^5.0.2", 30 | "bcrypt": "^5.1.1", 31 | "class-transformer": "^0.5.1", 32 | "class-validator": "^0.14.1", 33 | "reflect-metadata": "^0.2.0", 34 | "rxjs": "^7.8.1" 35 | }, 36 | "devDependencies": { 37 | "@nestjs/cli": "^10.0.0", 38 | "@nestjs/schematics": "^10.0.0", 39 | "@nestjs/testing": "^10.0.0", 40 | "@types/express": "^5.0.0", 41 | "@types/jest": "^29.5.2", 42 | "@types/node": "^20.3.1", 43 | "@types/supertest": "^6.0.0", 44 | "@typescript-eslint/eslint-plugin": "^8.0.0", 45 | "@typescript-eslint/parser": "^8.0.0", 46 | "eslint": "^8.0.0", 47 | "eslint-config-prettier": "^9.0.0", 48 | "eslint-plugin-prettier": "^5.0.0", 49 | "jest": "^29.5.0", 50 | "prettier": "^3.0.0", 51 | "source-map-support": "^0.5.21", 52 | "supertest": "^7.0.0", 53 | "ts-jest": "^29.1.0", 54 | "ts-loader": "^9.4.3", 55 | "ts-node": "^10.9.1", 56 | "tsconfig-paths": "^4.2.0", 57 | "typescript": "^5.1.3" 58 | }, 59 | "jest": { 60 | "moduleFileExtensions": [ 61 | "js", 62 | "json", 63 | "ts" 64 | ], 65 | "rootDir": "src", 66 | "testRegex": ".*\\.spec\\.ts$", 67 | "transform": { 68 | "^.+\\.(t|j)s$": "ts-jest" 69 | }, 70 | "collectCoverageFrom": [ 71 | "**/*.(t|j)s" 72 | ], 73 | "coverageDirectory": "../coverage", 74 | "testEnvironment": "node" 75 | } 76 | } 77 | -------------------------------------------------------------------------------- /src/products/admin/admin-products.service.ts: -------------------------------------------------------------------------------- 1 | import { Injectable } from '@nestjs/common'; 2 | import { CreateProductDto } from './dto/create-product.dto'; 3 | import { UpdateProductDto } from './dto/update-product.dto'; 4 | import { PrismaService } from '../../prisma/prisma.service'; 5 | import { ProductSlugAlreadyExistsError } from '../errors'; 6 | import { NotFoundError } from '../../common/errors'; 7 | 8 | @Injectable() 9 | export class AdminProductsService { 10 | constructor(private prismaService: PrismaService) {} 11 | 12 | async create(createProductDto: CreateProductDto) { 13 | const product = await this.prismaService.product.findFirst({ 14 | where: { 15 | slug: createProductDto.slug, 16 | }, 17 | }); 18 | 19 | if (product) { 20 | throw new ProductSlugAlreadyExistsError(createProductDto.slug); 21 | } 22 | 23 | return this.prismaService.product.create({ 24 | data: createProductDto, 25 | }); 26 | } 27 | 28 | findAll() { 29 | return this.prismaService.product.findMany(); 30 | } 31 | 32 | async findOne(id: string) { 33 | const product = await this.prismaService.product.findFirst({ 34 | where: { 35 | id, 36 | }, 37 | }); 38 | 39 | if (!product) { 40 | throw new NotFoundError('Product', id); 41 | } 42 | 43 | return product; 44 | } 45 | 46 | async update(id: string, updateProductDto: UpdateProductDto) { 47 | let product = null; 48 | 49 | if (updateProductDto.slug) { 50 | product = await this.prismaService.product.findFirst({ 51 | where: { 52 | slug: updateProductDto.slug, 53 | }, 54 | }); 55 | } 56 | 57 | if (product && product.id !== id) { 58 | throw new ProductSlugAlreadyExistsError(updateProductDto.slug); 59 | } 60 | 61 | product = 62 | product && product.id === id 63 | ? product 64 | : await this.prismaService.product.findFirst({ 65 | where: { 66 | id, 67 | }, 68 | }); 69 | 70 | if (!product) { 71 | throw new NotFoundError('Product', id); 72 | } 73 | 74 | return this.prismaService.product.update({ 75 | where: { 76 | id, 77 | }, 78 | data: updateProductDto, 79 | }); 80 | } 81 | 82 | async remove(id: string) { 83 | const product = await this.prismaService.product.findFirst({ 84 | where: { 85 | id, 86 | }, 87 | }); 88 | 89 | if (!product) { 90 | throw new NotFoundError('Product', id); 91 | } 92 | 93 | return this.prismaService.product.delete({ 94 | where: { 95 | id, 96 | }, 97 | }); 98 | } 99 | } 100 | --------------------------------------------------------------------------------