From 8f16cb4d3c8ee2e9647cb9921b8c254b703fb505 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Di=C3=AAgo=20de=20Barros?= Date: Tue, 25 Nov 2025 20:28:57 -0300 Subject: [PATCH 1/5] feat: create another docs endpoint dedicated to users and auth --- src/main.ts | 51 ++++++++++++++++++++++++++++++++++++++++++--------- 1 file changed, 42 insertions(+), 9 deletions(-) diff --git a/src/main.ts b/src/main.ts index 2e88b09..5d3b52c 100644 --- a/src/main.ts +++ b/src/main.ts @@ -2,6 +2,10 @@ import { NestFactory } from "@nestjs/core"; import { ValidationPipe, Logger } from "@nestjs/common"; import { SwaggerModule, DocumentBuilder } from "@nestjs/swagger"; import { AppModule } from "./app.module"; + +import { AuthModule } from "./auth/auth.module"; +import { UsersModule } from "./users/users.module"; + import cookieParser from "cookie-parser"; import { HttpExceptionFilter } from "./common/filters/http-exception.filter"; @@ -13,20 +17,46 @@ async function bootstrap() { app.use(cookieParser()); app.useGlobalFilters(new HttpExceptionFilter()); - const config = new DocumentBuilder() - .setTitle("Bolsa API") - .setDescription("API para sistema de bolsa de valores") + // ----------------------------------------------------- + // 1. DOCUMENTAÇÃO GERAL (TODOS OS ENDPOINTS) + // ----------------------------------------------------- + const globalConfig = new DocumentBuilder() + .setTitle("Bolsa API – Documentação Geral") + .setDescription("Documentação completa da API") .setVersion("1.0") - .addTag("Assets", "Operações relacionadas aos assets") + .addTag("Auth") + .addTag("Users") + .addTag("Assets") .build(); - const document = SwaggerModule.createDocument(app, config); - SwaggerModule.setup("api/docs", app, document, { - swaggerOptions: { - persistAuthorization: true, - }, + const globalDocument = SwaggerModule.createDocument(app, globalConfig); + + SwaggerModule.setup("api/docs", app, globalDocument, { + swaggerOptions: { persistAuthorization: true }, }); + // ----------------------------------------------------- + // 2. DOCUMENTAÇÃO SEPARADA PROPOSITALMENTE (AUTH + USERS) + // ----------------------------------------------------- + const coreConfig = new DocumentBuilder() + .setTitle("Bolsa API – Core (Auth + Users)") + .setDescription( + "Documentação apenas dos módulos de autenticação e usuários" + ) + .setVersion("1.0") + .addTag("Auth") + .addTag("Users") + .build(); + + const coreDocument = SwaggerModule.createDocument(app, coreConfig, { + include: [AuthModule, UsersModule], + }); + + SwaggerModule.setup("docs/core", app, coreDocument); + + // ----------------------------------------------------- + // CORS + // ----------------------------------------------------- const allowedOrigins = process.env.CORS_ORIGINS ? process.env.CORS_ORIGINS.split(",").map((o) => o.trim()) : []; @@ -37,6 +67,9 @@ async function bootstrap() { methods: ["GET", "POST", "PUT", "DELETE", "PATCH"], }); + // ----------------------------------------------------- + // VALIDATION PIPE + // ----------------------------------------------------- app.useGlobalPipes( new ValidationPipe({ whitelist: true, From 718f4a32a56eedcc6d20750554ce6a015addf002 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Di=C3=AAgo=20de=20Barros?= Date: Tue, 25 Nov 2025 20:29:38 -0300 Subject: [PATCH 2/5] feat: standardize auth module responses and it's docs --- src/assets/assets.service.ts | 98 ++++++++++++++++++------------------ src/auth/auth.controller.ts | 92 +++++++++++++++++---------------- src/auth/auth.service.ts | 3 +- src/auth/dto/login.dto.ts | 2 +- 4 files changed, 100 insertions(+), 95 deletions(-) diff --git a/src/assets/assets.service.ts b/src/assets/assets.service.ts index 368fab7..6df0c7d 100644 --- a/src/assets/assets.service.ts +++ b/src/assets/assets.service.ts @@ -1,13 +1,12 @@ -import { Injectable, BadRequestException } from '@nestjs/common'; -import { PrismaService } from '../common/prisma.service'; -import { CreateAssetDto } from './dto/create-asset.dto'; -import { UpdateAssetDto } from './dto/update-asset.dto'; -import { - AssetNotFoundException, - AssetAlreadyExistsException, - InvalidAssetDataException, - AssetServiceException -} from './exceptions/custom-exceptions'; +import { Injectable, BadRequestException } from "@nestjs/common"; +import { PrismaService } from "../common/prisma.service"; +import { CreateAssetDto } from "./dto/create-asset.dto"; +import { UpdateAssetDto } from "./dto/update-asset.dto"; +import { + AssetNotFoundException, + AssetAlreadyExistsException, + AssetServiceException, +} from "./exceptions/custom-exceptions"; @Injectable() export class AssetsService { @@ -15,32 +14,30 @@ export class AssetsService { async create(createAssetDto: CreateAssetDto) { try { - const existingAsset = await this.prisma.assets.findUnique({ - where: { symbol: createAssetDto.symbol.toUpperCase() } + where: { symbol: createAssetDto.symbol.toUpperCase() }, }); if (existingAsset) { throw new AssetAlreadyExistsException(createAssetDto.symbol); } - const asset = await this.prisma.assets.create({ data: { ...createAssetDto, symbol: createAssetDto.symbol.toUpperCase(), - } + }, }); return { ...asset, - price: parseFloat((asset as any).price.toString()) + price: parseFloat((asset as any).price.toString()), }; } catch (error) { if (error instanceof AssetAlreadyExistsException) { throw error; } - throw new AssetServiceException('Failed to create asset', error); + throw new AssetServiceException("Failed to create asset", error); } } @@ -48,26 +45,26 @@ export class AssetsService { try { const assets = await this.prisma.assets.findMany({ where: { isActive: true }, - orderBy: { createdAt: 'desc' } + orderBy: { createdAt: "desc" }, }); - - return assets.map(asset => ({ + + return assets.map((asset) => ({ ...asset, - price: parseFloat((asset as any).price.toString()) + price: parseFloat((asset as any).price.toString()), })); } catch (error) { - throw new AssetServiceException('Failed to fetch assets', error); + throw new AssetServiceException("Failed to fetch assets", error); } } async findOne(id: number) { try { if (!this.isValidId(id)) { - throw new BadRequestException('ID must be a positive number'); + throw new BadRequestException("ID must be a positive number"); } const asset = await this.prisma.assets.findUnique({ - where: { id } + where: { id }, }); if (!asset) { @@ -76,36 +73,39 @@ export class AssetsService { return { ...asset, - price: parseFloat((asset as any).price.toString()) + price: parseFloat((asset as any).price.toString()), }; } catch (error) { - if (error instanceof AssetNotFoundException || error instanceof BadRequestException) { + if ( + error instanceof AssetNotFoundException || + error instanceof BadRequestException + ) { throw error; } - throw new AssetServiceException('Failed to fetch asset', error); + throw new AssetServiceException("Failed to fetch asset", error); } } async update(id: number, updateAssetDto: UpdateAssetDto) { try { if (!this.isValidId(id)) { - throw new BadRequestException('ID must be a positive number'); + throw new BadRequestException("ID must be a positive number"); } - + if (Object.keys(updateAssetDto).length === 0) { - throw new BadRequestException('At least one field must be provided for update'); + throw new BadRequestException( + "At least one field must be provided for update" + ); } - await this.findOne(id); - if (updateAssetDto.symbol) { const existingAsset = await this.prisma.assets.findFirst({ - where: { + where: { symbol: updateAssetDto.symbol.toUpperCase(), - id: { not: id } - } + id: { not: id }, + }, }); if (existingAsset) { @@ -113,52 +113,54 @@ export class AssetsService { } } - const asset = await this.prisma.assets.update({ where: { id }, data: { ...updateAssetDto, symbol: updateAssetDto.symbol?.toUpperCase(), - } + }, }); return { ...asset, - price: parseFloat((asset as any).price.toString()) + price: parseFloat((asset as any).price.toString()), }; } catch (error) { - if (error instanceof AssetNotFoundException || - error instanceof AssetAlreadyExistsException || - error instanceof BadRequestException) { + if ( + error instanceof AssetNotFoundException || + error instanceof AssetAlreadyExistsException || + error instanceof BadRequestException + ) { throw error; } - throw new AssetServiceException('Failed to update asset', error); + throw new AssetServiceException("Failed to update asset", error); } } async remove(id: number) { try { if (!this.isValidId(id)) { - throw new BadRequestException('ID must be a positive number'); + throw new BadRequestException("ID must be a positive number"); } - await this.findOne(id); await this.prisma.assets.update({ where: { id }, - data: { isActive: false } + data: { isActive: false }, }); } catch (error) { - if (error instanceof AssetNotFoundException || error instanceof BadRequestException) { + if ( + error instanceof AssetNotFoundException || + error instanceof BadRequestException + ) { throw error; } - throw new AssetServiceException('Failed to remove asset', error); + throw new AssetServiceException("Failed to remove asset", error); } } private isValidId(id: number): boolean { return Number.isInteger(id) && id > 0; } - -} \ No newline at end of file +} diff --git a/src/auth/auth.controller.ts b/src/auth/auth.controller.ts index 83b3766..a27439d 100644 --- a/src/auth/auth.controller.ts +++ b/src/auth/auth.controller.ts @@ -1,4 +1,4 @@ -import { Controller, Post, Body, Get, Res } from "@nestjs/common"; +import { Controller, Post, Body, Get, Res, HttpCode } from "@nestjs/common"; import { type Response } from "express"; import { ApiBadRequestResponse, @@ -12,7 +12,7 @@ import { import { AuthService } from "./auth.service"; import { LoginDto } from "./dto/login.dto"; import { Public } from "./decorators/public.decorator"; -import { CurrentUser } from "./decorators/current-user.decorator"; +// import { CurrentUser } from "./decorators/current-user.decorator"; @ApiTags("Auth") @Controller("auth") @@ -21,6 +21,7 @@ export class AuthController { @Public() @Post("login") + @HttpCode(200) @ApiOperation({ summary: "Realiza login com email e senha", description: @@ -32,8 +33,8 @@ export class AuthController { exemplo: { summary: "Corpo do login", value: { - email: "user@example.com", - password: "123456", + email: "admin@bolsa.com", + password: "admin123", }, }, }, @@ -44,7 +45,7 @@ export class AuthController { example: { user: { id: 1, - email: "user@example.com", + email: "admin@bolsa.com", role: "user", }, }, @@ -55,7 +56,10 @@ export class AuthController { schema: { example: { statusCode: 400, - message: "Forneça um email válido", + message: [ + "email must be an email", + "password must be longer than or equal to 3 characters", + ], error: "Bad Request", }, }, @@ -91,42 +95,42 @@ export class AuthController { return { user }; } - @Get("profile") - @ApiCookieAuth() - @ApiOperation({ - summary: "Retorna dados do usuário autenticado", - description: "Endpoint protegido por JWT via cookie httpOnly.", - }) - @ApiOkResponse({ - description: "Retorna o usuário logado", - schema: { - example: { - message: "Dados do usuário autenticado", - user: { - id: 1, - email: "user@example.com", - role: "user", - }, - }, - }, - }) - @ApiUnauthorizedResponse({ - description: "Token inválido ou ausente", - schema: { - example: { - statusCode: 401, - message: "Unauthorized", - }, - }, - }) - async getProfile(@CurrentUser() user: any) { - return { - message: "Dados do usuário autenticado", - user: { - id: user.userId, - email: user.email, - role: user.role, - }, - }; - } + // @Get("profile") + // @ApiCookieAuth() + // @ApiOperation({ + // summary: "Retorna dados do usuário autenticado", + // description: "Endpoint protegido por JWT via cookie httpOnly.", + // }) + // @ApiOkResponse({ + // description: "Retorna o usuário logado", + // schema: { + // example: { + // message: "Dados do usuário autenticado", + // user: { + // id: 1, + // email: "admin@bolsa.com", + // role: "user", + // }, + // }, + // }, + // }) + // @ApiUnauthorizedResponse({ + // description: "Token inválido ou ausente", + // schema: { + // example: { + // statusCode: 401, + // message: "Unauthorized", + // }, + // }, + // }) + // async getProfile(@CurrentUser() user: any) { + // return { + // message: "Dados do usuário autenticado", + // user: { + // id: user.userId, + // email: user.email, + // role: user.role, + // }, + // }; + // } } diff --git a/src/auth/auth.service.ts b/src/auth/auth.service.ts index 4c213fa..c6c91a7 100644 --- a/src/auth/auth.service.ts +++ b/src/auth/auth.service.ts @@ -3,7 +3,6 @@ import { JwtService } from "@nestjs/jwt"; import { UsersService } from "../users/users.service"; import * as bcrypt from "bcryptjs"; - @Injectable() export class AuthService { constructor( @@ -28,7 +27,7 @@ export class AuthService { const user = await this.validateUser(email, password); if (!user) { - throw new UnauthorizedException("Credenciais inválidas"); + throw new UnauthorizedException("Email ou senha incorretos"); } const payload = { diff --git a/src/auth/dto/login.dto.ts b/src/auth/dto/login.dto.ts index dd9ff13..ef33bca 100644 --- a/src/auth/dto/login.dto.ts +++ b/src/auth/dto/login.dto.ts @@ -5,6 +5,6 @@ export class LoginDto { email: string; @IsString() - @MinLength(4) + @MinLength(6, { message: "Senha deve ter no mínimo 6 caracteres" }) password: string; } From b453b7d6162314f60729f2d6e9168ffed6d15362 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Di=C3=AAgo=20de=20Barros?= Date: Tue, 25 Nov 2025 22:15:07 -0300 Subject: [PATCH 3/5] chore: adds swagger schema docs for login dto --- src/auth/dto/login.dto.ts | 18 ++++++++++++++---- 1 file changed, 14 insertions(+), 4 deletions(-) diff --git a/src/auth/dto/login.dto.ts b/src/auth/dto/login.dto.ts index ef33bca..491c172 100644 --- a/src/auth/dto/login.dto.ts +++ b/src/auth/dto/login.dto.ts @@ -1,10 +1,20 @@ -import { IsEmail, IsString, MinLength } from "class-validator"; +import { IsEmail, IsNotEmpty } from "class-validator"; +import { ApiProperty } from "@nestjs/swagger"; export class LoginDto { - @IsEmail(undefined, { message: "Forneça um email válido" }) + @ApiProperty({ + example: "usuario@exemplo.com", + description: "Email do usuário para login.", + format: "email", + }) + @IsEmail({}, { message: "Forneça um email válido" }) email: string; - @IsString() - @MinLength(6, { message: "Senha deve ter no mínimo 6 caracteres" }) + @ApiProperty({ + example: "senha123", + description: "Senha do usuário.", + minLength: 1, + }) + @IsNotEmpty({ message: "Senha não pode estar vazia" }) password: string; } From 72a37f1c2e5679abf89743a25e6bf9f66ea2f43b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Di=C3=AAgo=20de=20Barros?= Date: Tue, 25 Nov 2025 22:15:50 -0300 Subject: [PATCH 4/5] chore: adds swagger user module docs --- src/users/dto/create-user.dto.ts | 21 +++- src/users/entities/user.entity.ts | 21 ++++ src/users/users.controller.ts | 168 ++++++++++++++++++++++-------- 3 files changed, 164 insertions(+), 46 deletions(-) create mode 100644 src/users/entities/user.entity.ts diff --git a/src/users/dto/create-user.dto.ts b/src/users/dto/create-user.dto.ts index 2131d55..d3dfcb0 100644 --- a/src/users/dto/create-user.dto.ts +++ b/src/users/dto/create-user.dto.ts @@ -1,13 +1,26 @@ -import { IsEmail, IsNotEmpty, MinLength } from 'class-validator'; +import { IsEmail, IsNotEmpty, MinLength } from "class-validator"; +import { ApiProperty } from "@nestjs/swagger"; export class CreateUserDto { - @IsEmail() + @ApiProperty({ + example: "usuario@exemplo.com", + description: "Email válido do usuário", + }) + @IsEmail({}, { message: "Forneça um email válido" }) email: string; + @ApiProperty({ + example: "senha123", + description: "Senha do usuário (mínimo 6 caracteres)", + }) @IsNotEmpty() - @MinLength(6) + @MinLength(6, { message: "Senha deve ter no mínimo 6 caracteres" }) password: string; - @IsNotEmpty() + @ApiProperty({ + example: "João da Silva", + description: "Nome completo do usuário", + }) + @IsNotEmpty({ message: "Nome não pode estar vazio" }) name: string; } diff --git a/src/users/entities/user.entity.ts b/src/users/entities/user.entity.ts new file mode 100644 index 0000000..0ff9742 --- /dev/null +++ b/src/users/entities/user.entity.ts @@ -0,0 +1,21 @@ +import { ApiProperty } from "@nestjs/swagger"; + +export class UserEntity { + @ApiProperty({ example: "1" }) + id: string; + + @ApiProperty({ example: "usuario@exemplo.com" }) + email: string; + + @ApiProperty({ example: "João da Silva" }) + name: string; + + @ApiProperty({ example: "USER", enum: ["USER", "ADMIN"] }) + role: string; + + @ApiProperty({ example: "2025-01-20T12:40:00.000Z" }) + createdAt: Date; + + @ApiProperty({ example: "2025-01-20T12:45:00.000Z" }) + updatedAt: Date; +} diff --git a/src/users/users.controller.ts b/src/users/users.controller.ts index c1fbf49..9cf7e21 100644 --- a/src/users/users.controller.ts +++ b/src/users/users.controller.ts @@ -1,81 +1,165 @@ -import { Body, Controller, Post, Get, Param, Delete } from "@nestjs/common"; +import { + Body, + Controller, + Post, + Get, + Param, + Delete, + HttpCode, +} from "@nestjs/common"; +import { + ApiTags, + ApiOperation, + ApiCreatedResponse, + ApiOkResponse, + ApiBadRequestResponse, + ApiUnauthorizedResponse, + ApiForbiddenResponse, + ApiNotFoundResponse, + ApiBody, + ApiExtraModels, + getSchemaPath, +} from "@nestjs/swagger"; + import { UsersService } from "./users.service"; import { CreateUserDto } from "./dto/create-user.dto"; +import { UserEntity } from "./entities/user.entity"; + import { Public } from "../auth/decorators/public.decorator"; import { Roles } from "../auth/decorators/roles.decorator"; import { CurrentUser } from "../auth/decorators/current-user.decorator"; -/** - * 👥 Users Controller - * - * Gerencia usuários da plataforma. - * - * NÍVEIS DE ACESSO: - * - POST /users (registro): Público (qualquer pessoa pode se registrar) - * - GET /users (listar): Apenas ADMIN - * - GET /users/me (perfil próprio): Qualquer usuário autenticado - * - GET /users/:id (perfil específico): Apenas ADMIN - * - DELETE /users/:id: Apenas ADMIN - * - * DEMONSTRAÇÃO DIDÁTICA: - * Este controller mostra os 3 níveis de acesso: - * 1. Público (@Public) - * 2. Autenticado (sem decorator = qualquer role) - * 3. Restrito por role (@Roles) - */ +@ApiTags("Users") +@ApiExtraModels(UserEntity) @Controller("users") export class UsersController { constructor(private readonly usersService: UsersService) {} - /** - * 🌍 Registrar novo usuário - Rota PÚBLICA - * @Public() permite acesso sem autenticação - * Necessário para que novos usuários possam se cadastrar - */ + // ------------------------------------------------ + // 📌 REGISTRAR USUÁRIO (PÚBLICO) + // ------------------------------------------------ @Public() @Post() + @ApiOperation({ + summary: "Registrar novo usuário", + description: "Cria um novo usuário com role padrão USER.", + }) + @ApiBody({ type: CreateUserDto }) + @ApiCreatedResponse({ + description: "Usuário criado com sucesso", + schema: { $ref: getSchemaPath(UserEntity) }, + }) + @ApiBadRequestResponse({ + description: "Erro de validação no DTO", + schema: { + example: { + statusCode: 400, + message: [ + "Forneça um email válido", + "Senha deve ter no mínimo 6 caracteres", + "Nome não pode estar vazio", + ], + error: "Bad Request", + }, + }, + }) async create(@Body() createUserDto: CreateUserDto) { return this.usersService.create(createUserDto); } - /** - * 👮 Listar todos os usuários - Apenas ADMIN - * Informação sensível que só administradores devem ver - */ + // ------------------------------------------------ + // 👮 LISTAR TODOS (ADMIN) + // ------------------------------------------------ @Roles("ADMIN") @Get() + @ApiOperation({ + summary: "Listar todos os usuários", + description: "Apenas administradores podem acessar esta rota.", + }) + @ApiOkResponse({ + description: "Lista de usuários retornada com sucesso", + schema: { + type: "array", + items: { $ref: getSchemaPath(UserEntity) }, + }, + }) + @ApiUnauthorizedResponse({ + description: "Token inválido ou ausente", + }) + @ApiForbiddenResponse({ + description: "Acesso negado — apenas administradores", + }) async findAll(@CurrentUser() admin: any) { - console.log(`Admin ${admin.email} está listando todos os usuários`); return this.usersService.findAll(); } - /** - * 👤 Ver próprio perfil - Qualquer usuário autenticado - * Sem @Roles() = qualquer role pode acessar - * Usa @CurrentUser() para pegar ID do usuário logado - */ + // ------------------------------------------------ + // 👤 VER PERFIL PRÓPRIO (AUTENTICADO) + // ------------------------------------------------ @Get("me") + @ApiOperation({ + summary: "Obter o próprio perfil", + }) + @ApiOkResponse({ + description: "Perfil retornado com sucesso", + schema: { $ref: getSchemaPath(UserEntity) }, + }) + @ApiUnauthorizedResponse({ + description: "Token inválido ou ausente", + }) async getMyProfile(@CurrentUser("userId") userId: string) { return this.usersService.findOne(userId); } - /** - * 👮 Ver perfil de outro usuário - Apenas ADMIN - * Administradores podem ver perfil de qualquer usuário - */ + // ------------------------------------------------ + // 👮 VER USUÁRIO ESPECÍFICO (ADMIN) + // ------------------------------------------------ @Roles("ADMIN") @Get(":id") + @ApiOperation({ + summary: "Buscar usuário por ID", + description: "Apenas administradores podem acessar esta rota.", + }) + @ApiOkResponse({ + description: "Usuário encontrado", + schema: { $ref: getSchemaPath(UserEntity) }, + }) + @ApiNotFoundResponse({ + description: "Usuário não encontrado", + schema: { + example: { + statusCode: 404, + message: "Usuário não encontrado", + error: "Not Found", + }, + }, + }) + @ApiForbiddenResponse({ + description: "Acesso negado — apenas administradores", + }) async findOne(@Param("id") id: string) { return this.usersService.findOne(id); } - /** - * 👮 Deletar usuário - Apenas ADMIN - */ + // ------------------------------------------------ + // 👮 DELETAR USUÁRIO (ADMIN) + // ------------------------------------------------ @Roles("ADMIN") @Delete(":id") + @ApiOperation({ + summary: "Deletar usuário", + description: "Apenas administradores podem excluir usuários.", + }) + @ApiOkResponse({ + description: "Usuário deletado com sucesso", + }) + @ApiNotFoundResponse({ + description: "Usuário não encontrado", + }) + @ApiForbiddenResponse({ + description: "Acesso negado — apenas administradores", + }) async remove(@Param("id") id: string, @CurrentUser() admin: any) { - console.log(`Admin ${admin.email} está deletando usuário ${id}`); return this.usersService.remove(id); } } From 0a5b671912175a0a68ec22bde056bfb0aec28f4f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Di=C3=AAgo=20de=20Barros?= Date: Tue, 25 Nov 2025 22:16:51 -0300 Subject: [PATCH 5/5] Functionality: Adds custom 401 and 403 messages to JWT Auth Guard and Role Guard. --- src/auth/decorators/current-user.decorator.ts | 16 ++++--- src/auth/guards/jwt-auth.guard.ts | 38 ++++----------- src/auth/guards/roles.guard.ts | 48 ++++++++----------- 3 files changed, 36 insertions(+), 66 deletions(-) diff --git a/src/auth/decorators/current-user.decorator.ts b/src/auth/decorators/current-user.decorator.ts index d9884f3..07bd378 100644 --- a/src/auth/decorators/current-user.decorator.ts +++ b/src/auth/decorators/current-user.decorator.ts @@ -1,4 +1,8 @@ -import { createParamDecorator, ExecutionContext } from "@nestjs/common"; +import { + createParamDecorator, + ExecutionContext, + UnauthorizedException, +} from "@nestjs/common"; /** * 👤 Current User Decorator @@ -36,20 +40,18 @@ import { createParamDecorator, ExecutionContext } from "@nestjs/common"; */ export const CurrentUser = createParamDecorator( (data: string | undefined, context: ExecutionContext) => { - // 📦 Extrai o objeto request do contexto HTTP const request = context.switchToHttp().getRequest(); - // 👤 Pega o usuário do request (anexado pela JwtStrategy) const user = request.user; - // 🎯 Se data foi especificado, retorna apenas essa propriedade - // Exemplo: @CurrentUser('email') => retorna apenas user.email + if (!user) { + throw new UnauthorizedException("JWT inválido ou ausente."); + } + if (data) { return user?.[data]; } - // 📦 Se data não foi especificado, retorna o usuário completo - // Exemplo: @CurrentUser() => retorna { userId, email, role } return user; } ); diff --git a/src/auth/guards/jwt-auth.guard.ts b/src/auth/guards/jwt-auth.guard.ts index f602378..b455ece 100644 --- a/src/auth/guards/jwt-auth.guard.ts +++ b/src/auth/guards/jwt-auth.guard.ts @@ -1,60 +1,38 @@ -import { Injectable, ExecutionContext } from "@nestjs/common"; +import { + Injectable, + ExecutionContext, + UnauthorizedException, +} from "@nestjs/common"; import { AuthGuard } from "@nestjs/passport"; import { Reflector } from "@nestjs/core"; import { Observable } from "rxjs"; -/** - * 🔐 JWT Authentication Guard - * - * Este guard é responsável pela AUTENTICAÇÃO (verificar SE o usuário está logado). - * Ele estende o AuthGuard do Passport que automaticamente: - * 1. Extrai o token do header Authorization - * 2. Valida o token usando a JwtStrategy - * 3. Anexa os dados do usuário em req.user - * - * FUNCIONALIDADE ADICIONAL: - * - Permite marcar rotas como públicas usando o decorator @Public() - * - Rotas públicas pulam a autenticação - */ @Injectable() export class JwtAuthGuard extends AuthGuard("jwt") { constructor(private reflector: Reflector) { super(); } - /** - * 🧠 Método chamado ANTES de validar o token - * - * ExecutionContext: Contexto da requisição que permite acessar: - * - getHandler(): método do controller sendo chamado - * - getClass(): classe do controller - * - switchToHttp().getRequest(): objeto request do Express - */ canActivate( context: ExecutionContext ): boolean | Promise | Observable { - // 🔍 Verifica se a rota está marcada com @Public() - // getAllAndOverride busca o metadata em dois lugares (ordem de prioridade): - // 1. No método (handler) - exemplo: @Get() @Public() - // 2. Na classe (controller) - exemplo: @Controller() @Public() const isPublic = this.reflector.getAllAndOverride("isPublic", [ context.getHandler(), context.getClass(), ]); - // ✅ Se a rota é pública, permite acesso sem autenticação if (isPublic) { return true; } - // 🔐 Se não é pública, delega para o AuthGuard do Passport - // que irá validar o JWT usando a JwtStrategy return super.canActivate(context); } handleRequest(err, user, info) { if (err || !user) { - return null; // vai gerar 401 automaticamente + throw new UnauthorizedException( + "Não autorizado. JWT inválido ou ausente." + ); } return user; } diff --git a/src/auth/guards/roles.guard.ts b/src/auth/guards/roles.guard.ts index 9db3d75..cd3354e 100644 --- a/src/auth/guards/roles.guard.ts +++ b/src/auth/guards/roles.guard.ts @@ -1,58 +1,48 @@ -import { Injectable, CanActivate, ExecutionContext } from "@nestjs/common"; +import { + Injectable, + CanActivate, + ExecutionContext, + ForbiddenException, +} from "@nestjs/common"; import { Reflector } from "@nestjs/core"; -/** - * 👮 Roles Guard - * - * Este guard é responsável pela AUTORIZAÇÃO (verificar O QUE o usuário pode fazer). - * Ele verifica se o usuário autenticado possui as roles (papéis) necessárias. - * - * IMPORTANTE: - * - Este guard deve ser executado APÓS o JwtAuthGuard - * - Assume que req.user já foi populado pela JwtStrategy - * - Se nenhuma role for especificada, permite acesso (rota protegida apenas por autenticação) - * - * EXEMPLO DE USO: - * @Roles('ADMIN') // Apenas admins - * @Roles('ADMIN', 'USER') // Admins OU usuários comuns - */ @Injectable() export class RolesGuard implements CanActivate { constructor(private reflector: Reflector) {} /** - * 🎯 Determina se a requisição pode prosseguir + * Determina se a requisição pode prosseguir * * @param context - Contexto de execução da requisição * @returns true se autorizado, false caso contrário */ canActivate(context: ExecutionContext): boolean { - // 🔍 Extrai as roles requeridas do metadata definido por @Roles() - // getAllAndOverride busca primeiro no método, depois na classe const requiredRoles = this.reflector.getAllAndOverride("roles", [ context.getHandler(), context.getClass(), ]); - // ✅ Se não há roles requeridas, permite acesso - // (a rota está protegida apenas por autenticação, não por role) if (!requiredRoles || requiredRoles.length === 0) { return true; } - // 📦 Extrai o objeto request do contexto HTTP const request = context.switchToHttp().getRequest(); - - // 👤 Pega o usuário do request (foi anexado pela JwtStrategy) const user = request.user; - // 🚫 Se não há usuário (não deveria acontecer se JwtAuthGuard passou) if (!user || !user.role) { - return false; + throw new ForbiddenException( + "Você não tem permissão para acessar este recurso." + ); + } + + const hasPermission = requiredRoles.includes(user.role); + + if (!hasPermission) { + throw new ForbiddenException( + "Você não tem permissão para acessar este recurso." + ); } - // ✅ Verifica se a role do usuário está na lista de roles permitidas - // some() retorna true se pelo menos uma condição for verdadeira - return requiredRoles.some((role) => user.role === role); + return true; } }