From ee1e3bd7f8fbddffabbea28c2d73ce8cc6af4122 Mon Sep 17 00:00:00 2001 From: LeoMortari Date: Thu, 11 Sep 2025 17:51:28 -0300 Subject: [PATCH] Adiciona primeiras partes da autenticacao --- package.json | 2 + src/app.module.ts | 17 +++-- src/auth/auth.controller.spec.ts | 18 +++++ src/auth/auth.controller.ts | 29 ++++++++ src/auth/auth.module.ts | 10 +++ src/auth/auth.service.spec.ts | 18 +++++ src/auth/auth.service.ts | 103 ++++++++++++++++++++++++++++ src/auth/dto/login.dto.ts | 16 +++++ src/auth/dto/loginResponse.dto.ts | 8 +++ src/middleware/logger.middleware.ts | 13 ++++ src/redis/redis.module.ts | 0 11 files changed, 228 insertions(+), 6 deletions(-) create mode 100644 src/auth/auth.controller.spec.ts create mode 100644 src/auth/auth.controller.ts create mode 100644 src/auth/auth.module.ts create mode 100644 src/auth/auth.service.spec.ts create mode 100644 src/auth/auth.service.ts create mode 100644 src/auth/dto/login.dto.ts create mode 100644 src/auth/dto/loginResponse.dto.ts create mode 100644 src/middleware/logger.middleware.ts delete mode 100644 src/redis/redis.module.ts diff --git a/package.json b/package.json index 78d2769..ef1d31e 100644 --- a/package.json +++ b/package.json @@ -21,9 +21,11 @@ }, "dependencies": { "@nestjs/common": "11.0.1", + "@nestjs/config": "4.0.2", "@nestjs/core": "11.0.1", "@nestjs/platform-express": "11.0.1", "@prisma/client": "6.14.0", + "axios": "1.12.0", "bcrypt": "6.0.0", "class-transformer": "0.5.1", "class-validator": "0.14.2", diff --git a/src/app.module.ts b/src/app.module.ts index b784c99..d9c6a4b 100644 --- a/src/app.module.ts +++ b/src/app.module.ts @@ -1,23 +1,28 @@ -import { Module } from '@nestjs/common'; +import { Module, MiddlewareConsumer } from '@nestjs/common'; import { ConfigModule } from '@nestjs/config'; + import { PrismaModule } from './prisma/prisma.module'; import { AppController } from './app.controller'; import { AppService } from './app.service'; import { VideosModule } from './videos/videos.module'; -import { UsuariosModule } from './usuarios/usuarios.module'; -import { RedisModule } from './redis/redis.module'; import { AuthModule } from './auth/auth.module'; +import { VideosController } from './videos/videos.controller'; +import { UsuariosModule } from './usuarios/usuarios.module'; +import { LoggerMiddleware } from './middleware/logger.middleware'; @Module({ imports: [ ConfigModule.forRoot({ isGlobal: true }), PrismaModule, - RedisModule, - AuthModule, VideosModule, + AuthModule, UsuariosModule, ], controllers: [AppController], providers: [AppService], }) -export class AppModule {} +export class AppModule { + configure(consumer: MiddlewareConsumer) { + consumer.apply(LoggerMiddleware).forRoutes(VideosController); + } +} diff --git a/src/auth/auth.controller.spec.ts b/src/auth/auth.controller.spec.ts new file mode 100644 index 0000000..27a31e6 --- /dev/null +++ b/src/auth/auth.controller.spec.ts @@ -0,0 +1,18 @@ +import { Test, TestingModule } from '@nestjs/testing'; +import { AuthController } from './auth.controller'; + +describe('AuthController', () => { + let controller: AuthController; + + beforeEach(async () => { + const module: TestingModule = await Test.createTestingModule({ + controllers: [AuthController], + }).compile(); + + controller = module.get(AuthController); + }); + + it('should be defined', () => { + expect(controller).toBeDefined(); + }); +}); diff --git a/src/auth/auth.controller.ts b/src/auth/auth.controller.ts new file mode 100644 index 0000000..9bc8628 --- /dev/null +++ b/src/auth/auth.controller.ts @@ -0,0 +1,29 @@ +import { Controller, Post, Body, HttpCode, HttpStatus } from '@nestjs/common'; + +import LoginResponseDto from './dto/loginResponse.dto'; + +import { AuthService } from './auth.service'; +import { LoginDto } from './dto/login.dto'; + +@Controller('auth') +export class AuthController { + constructor(private readonly authService: AuthService) {} + + @Post('login') + async login(@Body() loginDto: LoginDto): Promise { + return this.authService.login(loginDto); + } + + @Post('logout') + @HttpCode(HttpStatus.OK) + async logout(@Body() body: { refreshToken: string }): Promise { + return this.authService.logout(body.refreshToken); + } + + @Post('refresh-token') + async refreshToken( + @Body() body: { refreshToken: string }, + ): Promise { + return this.authService.refreshToken(body.refreshToken); + } +} diff --git a/src/auth/auth.module.ts b/src/auth/auth.module.ts new file mode 100644 index 0000000..b8219aa --- /dev/null +++ b/src/auth/auth.module.ts @@ -0,0 +1,10 @@ +import { Module } from '@nestjs/common'; +import { AuthService } from './auth.service'; +import { AuthController } from './auth.controller'; + +@Module({ + controllers: [AuthController], + providers: [AuthService], + exports: [AuthService], +}) +export class AuthModule {} diff --git a/src/auth/auth.service.spec.ts b/src/auth/auth.service.spec.ts new file mode 100644 index 0000000..800ab66 --- /dev/null +++ b/src/auth/auth.service.spec.ts @@ -0,0 +1,18 @@ +import { Test, TestingModule } from '@nestjs/testing'; +import { AuthService } from './auth.service'; + +describe('AuthService', () => { + let service: AuthService; + + beforeEach(async () => { + const module: TestingModule = await Test.createTestingModule({ + providers: [AuthService], + }).compile(); + + service = module.get(AuthService); + }); + + it('should be defined', () => { + expect(service).toBeDefined(); + }); +}); diff --git a/src/auth/auth.service.ts b/src/auth/auth.service.ts new file mode 100644 index 0000000..a48cee0 --- /dev/null +++ b/src/auth/auth.service.ts @@ -0,0 +1,103 @@ +import axios, { isAxiosError } from 'axios'; + +import { Injectable, UnauthorizedException } from '@nestjs/common'; + +import { LoginDto } from './dto/login.dto'; + +import LoginResponseDto from './dto/loginResponse.dto'; + +@Injectable() +export class AuthService { + private readonly keycloakUrl = + 'https://auth.clipperia.com.br/realms/clipperia/protocol/openid-connect'; + private readonly clientId = 'usuarios'; + private readonly clientSecret = process.env.KEYCLOAK_CLIENT_SECRET; + + private readonly keycloakApi = axios.create({ + baseURL: this.keycloakUrl, + headers: { + 'Content-Type': 'application/x-www-form-urlencoded', + }, + }); + + async login(loginDto: LoginDto) { + try { + const params = new URLSearchParams({ + client_id: this.clientId, + grant_type: 'password', + username: loginDto.username, + password: loginDto.password, + }); + + if (this.clientSecret) { + params.append('client_secret', this.clientSecret); + } + + const { data } = await this.keycloakApi.post( + '/token', + params, + ); + + return data; + } catch (error) { + if (isAxiosError(error)) { + console.error(error.response?.data); + throw new UnauthorizedException(error.response?.data); + } + + throw new UnauthorizedException('Usuário ou senha inválidos'); + } + } + + async logout(refreshToken: string): Promise { + try { + const data = new URLSearchParams({ + client_id: this.clientId, + refresh_token: refreshToken, + }); + + if (this.clientSecret) { + data.append('client_secret', this.clientSecret); + } + + await this.keycloakApi.post('/logout', data); + } catch (error) { + if (isAxiosError(error)) { + console.error(error.response?.data); + throw new UnauthorizedException(error.response?.data); + } + + throw new UnauthorizedException('Erro ao deslogar usuário'); + } + } + + async refreshToken(refreshToken: string) { + console.log(refreshToken); + + try { + const params = new URLSearchParams({ + client_id: this.clientId, + grant_type: 'refresh_token', + refresh_token: refreshToken, + }); + + if (this.clientSecret) { + params.append('client_secret', this.clientSecret); + } + + const { data } = await this.keycloakApi.post( + '/token', + params, + ); + + return data; + } catch (error) { + if (isAxiosError(error)) { + console.error(error.response?.data); + throw new UnauthorizedException(error.response?.data); + } + + throw new UnauthorizedException('Erro ao renovar token'); + } + } +} diff --git a/src/auth/dto/login.dto.ts b/src/auth/dto/login.dto.ts new file mode 100644 index 0000000..b30792a --- /dev/null +++ b/src/auth/dto/login.dto.ts @@ -0,0 +1,16 @@ +import { IsEmail, IsNotEmpty, IsString } from 'class-validator'; + +export class LoginDto { + @IsString() + @IsNotEmpty() + username: string; + + @IsString() + @IsEmail() + @IsNotEmpty() + email: string; + + @IsString() + @IsNotEmpty() + password: string; +} diff --git a/src/auth/dto/loginResponse.dto.ts b/src/auth/dto/loginResponse.dto.ts new file mode 100644 index 0000000..a16dc04 --- /dev/null +++ b/src/auth/dto/loginResponse.dto.ts @@ -0,0 +1,8 @@ +export default class LoginResponseDto { + access_token: string; + refresh_token: string; + token_type: string; + expires_in: number; + refresh_expires_in: number; + scope: string; +} diff --git a/src/middleware/logger.middleware.ts b/src/middleware/logger.middleware.ts new file mode 100644 index 0000000..daa82b5 --- /dev/null +++ b/src/middleware/logger.middleware.ts @@ -0,0 +1,13 @@ +import { Injectable, NestMiddleware } from '@nestjs/common'; +import { Request, Response, NextFunction } from 'express'; + +@Injectable() +export class LoggerMiddleware implements NestMiddleware { + use(req: Request, res: Response, next: NextFunction) { + console.table({ + method: req.method, + url: req.url, + }); + next(); + } +} diff --git a/src/redis/redis.module.ts b/src/redis/redis.module.ts deleted file mode 100644 index e69de29..0000000