From d2dbbc4103207cf8955ed0b27fa6e4622136ac19 Mon Sep 17 00:00:00 2001 From: Gabriel Viell Castilho Date: Wed, 15 Apr 2026 11:42:13 -0300 Subject: [PATCH 1/3] feat:reset password --- backend/src/modules/auth/auth.controller.ts | 47 +++++++++++++++++++ backend/src/modules/auth/auth.service.ts | 45 ++++++++++++++++++ .../category/dtos/forgotPassword.dto.ts | 6 +++ .../category/dtos/resetPassword.dto.ts | 21 +++++++++ backend/src/modules/user/user.service.ts | 1 + 5 files changed, 120 insertions(+) create mode 100644 backend/src/modules/category/dtos/forgotPassword.dto.ts create mode 100644 backend/src/modules/category/dtos/resetPassword.dto.ts diff --git a/backend/src/modules/auth/auth.controller.ts b/backend/src/modules/auth/auth.controller.ts index 50cb0f1..e7a2408 100644 --- a/backend/src/modules/auth/auth.controller.ts +++ b/backend/src/modules/auth/auth.controller.ts @@ -119,4 +119,51 @@ export class AuthController { login(@Body() user: ExistingUserDTO): Promise<{ token: string } | null> { return this.authService.login(user); } + + @Post('forgot-password') + @HttpCode(HttpStatus.OK) + @ApiOperation({ summary: 'Solicitar recuperação de senha' }) + @ApiBody({ + schema: { + example: { + email: 'usuario@email.com', + }, + }, + }) + @ApiResponse({ + status: 200, + description: + 'Se o email existir, um link de recuperação será enviado', + }) + forgotPassword(@Body() body: { email: string }): Promise { + return this.authService.forgotPassword(body.email); + } + + @Post('reset-password') + @HttpCode(HttpStatus.OK) + @ApiOperation({ summary: 'Redefinir senha do usuário' }) + @ApiBody({ + schema: { + example: { + token: 'jwt.token.aqui', + newPassword: 'NovaSenha@123', + }, + }, + }) + @ApiResponse({ + status: 200, + description: 'Senha redefinida com sucesso', + }) + @ApiResponse({ + status: 400, + description: 'Token inválido ou expirado', + }) + resetPassword( + @Body() body: { token: string; newPassword: string }, + ): Promise { + return this.authService.resetPassword( + body.token, + body.newPassword, + ); + } } diff --git a/backend/src/modules/auth/auth.service.ts b/backend/src/modules/auth/auth.service.ts index 1bce57c..835a104 100644 --- a/backend/src/modules/auth/auth.service.ts +++ b/backend/src/modules/auth/auth.service.ts @@ -101,4 +101,49 @@ export class AuthService { await this.register(admin); } } + + async forgotPassword(email: string): Promise { + const user = await this.userService.findByEmail(email); + + if (!user) return; + + const payload = { + sub: user._id, + email: user.email, + type: 'reset-password', + }; + + const token = await this.jwtService.signAsync(payload, { + expiresIn: '15m', + }); + + console.log(`Reset token: ${token}`); + + // substituir por envio real de email + } + + async resetPassword(token: string, newPassword: string): Promise { + try { + const payload = await this.jwtService.verifyAsync(token); + + if (payload.type !== 'reset-password') { + throw new BadRequestException('Token inválido'); + } + + const user = await this.userService.findById(payload.sub); + + if (!user) { + throw new BadRequestException('Usuário não encontrado'); + } + + const hashedPassword = await this.hashPassword(newPassword); + + await this.userService.updateUser(payload.sub, { + password: hashedPassword, + }); + + } catch (error) { + throw new BadRequestException('Token inválido ou expirado'); + } + } } diff --git a/backend/src/modules/category/dtos/forgotPassword.dto.ts b/backend/src/modules/category/dtos/forgotPassword.dto.ts new file mode 100644 index 0000000..595d7a7 --- /dev/null +++ b/backend/src/modules/category/dtos/forgotPassword.dto.ts @@ -0,0 +1,6 @@ +import { IsEmail } from 'class-validator'; + +export class ForgotPasswordDTO { + @IsEmail() + email: string; +} \ No newline at end of file diff --git a/backend/src/modules/category/dtos/resetPassword.dto.ts b/backend/src/modules/category/dtos/resetPassword.dto.ts new file mode 100644 index 0000000..bdee220 --- /dev/null +++ b/backend/src/modules/category/dtos/resetPassword.dto.ts @@ -0,0 +1,21 @@ +import { IsString, IsStrongPassword } from 'class-validator'; + +export class ResetPasswordDTO { + @IsString() + token: string; + + @IsStrongPassword( + { + minLength: 8, + minLowercase: 1, + minUppercase: 1, + minNumbers: 1, + minSymbols: 1, + }, + { + message: + 'Password must be at least 8 characters long and include uppercase, lowercase, number and symbol.', + }, + ) + newPassword: string; +} \ No newline at end of file diff --git a/backend/src/modules/user/user.service.ts b/backend/src/modules/user/user.service.ts index ab13383..6bcf2d3 100644 --- a/backend/src/modules/user/user.service.ts +++ b/backend/src/modules/user/user.service.ts @@ -145,6 +145,7 @@ export class UserService { data: Partial<{ name: string; email: string; + password: string; role: UserRole; companyId: string; categories: string[]; From 5ca2f75b4f4bd900b4a22b7c89a305d34294883b Mon Sep 17 00:00:00 2001 From: Gabriel Viell Castilho Date: Wed, 15 Apr 2026 17:54:34 -0300 Subject: [PATCH 2/3] feat: send email --- backend/package-lock.json | 10 +++++++ backend/package.json | 1 + backend/src/app.module.ts | 2 ++ backend/src/modules/auth/auth.module.ts | 2 ++ backend/src/modules/auth/auth.service.ts | 9 ++++-- backend/src/modules/email/email.controller.ts | 7 +++++ backend/src/modules/email/email.module.ts | 10 +++++++ backend/src/modules/email/email.service.ts | 29 +++++++++++++++++++ 8 files changed, 67 insertions(+), 3 deletions(-) create mode 100644 backend/src/modules/email/email.controller.ts create mode 100644 backend/src/modules/email/email.module.ts create mode 100644 backend/src/modules/email/email.service.ts diff --git a/backend/package-lock.json b/backend/package-lock.json index d9d427b..997cc42 100644 --- a/backend/package-lock.json +++ b/backend/package-lock.json @@ -25,6 +25,7 @@ "mongoose": "^9.3.1", "morgan": "^1.10.1", "node-nlp": "^5.0.0-alpha.5", + "nodemailer": "^8.0.5", "passport": "^0.7.0", "passport-jwt": "^4.0.1", "reflect-metadata": "^0.2.2", @@ -9641,6 +9642,15 @@ "dev": true, "license": "MIT" }, + "node_modules/nodemailer": { + "version": "8.0.5", + "resolved": "https://registry.npmjs.org/nodemailer/-/nodemailer-8.0.5.tgz", + "integrity": "sha512-0PF8Yb1yZuQfQbq+5/pZJrtF6WQcjTd5/S4JOHs9PGFxuTqoB/icwuB44pOdURHJbRKX1PPoJZtY7R4VUoCC8w==", + "license": "MIT-0", + "engines": { + "node": ">=6.0.0" + } + }, "node_modules/normalize-path": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz", diff --git a/backend/package.json b/backend/package.json index 9bf4ec0..c5634f1 100644 --- a/backend/package.json +++ b/backend/package.json @@ -37,6 +37,7 @@ "mongoose": "^9.3.1", "morgan": "^1.10.1", "node-nlp": "^5.0.0-alpha.5", + "nodemailer": "^8.0.5", "passport": "^0.7.0", "passport-jwt": "^4.0.1", "reflect-metadata": "^0.2.2", diff --git a/backend/src/app.module.ts b/backend/src/app.module.ts index 91d3125..b665bec 100644 --- a/backend/src/app.module.ts +++ b/backend/src/app.module.ts @@ -10,6 +10,7 @@ import { ChatModule } from './modules/chat/chat.module'; import { TriageModule } from './modules/triage/triage.module'; import { CategoryModule } from './modules/category/category.module'; import { TicketModule } from './modules/ticket/ticket.module'; +import { EmailModule } from './modules/email/email.module'; @Module({ imports: [ @@ -26,6 +27,7 @@ import { TicketModule } from './modules/ticket/ticket.module'; TriageModule, CategoryModule, TicketModule, + EmailModule, ], controllers: [], providers: [], diff --git a/backend/src/modules/auth/auth.module.ts b/backend/src/modules/auth/auth.module.ts index da8312c..ca1e7c7 100644 --- a/backend/src/modules/auth/auth.module.ts +++ b/backend/src/modules/auth/auth.module.ts @@ -5,10 +5,12 @@ import { UserModule } from '../user/user.module'; import { JwtModule } from '@nestjs/jwt'; import { JwtGuard } from './guards/jwt.guard'; import { JwtStrategy } from './guards/jwt.strategy'; +import { EmailModule } from '../email/email.module'; @Module({ imports: [ UserModule, + EmailModule, JwtModule.registerAsync({ useFactory: () => ({ secret: 'secret', diff --git a/backend/src/modules/auth/auth.service.ts b/backend/src/modules/auth/auth.service.ts index 835a104..1a90609 100644 --- a/backend/src/modules/auth/auth.service.ts +++ b/backend/src/modules/auth/auth.service.ts @@ -11,12 +11,14 @@ import { UserDetails } from '../user/user.interface'; import { ExistingUserDTO } from '../user/dtos/existingUserDTO'; import { JwtService } from '@nestjs/jwt'; import { UserRole } from '../user/user.schema'; +import { EmailService } from '../email/email.service'; @Injectable() export class AuthService { constructor( private userService: UserService, private jwtService: JwtService, + private emailService: EmailService, ) {} async hashPassword(password: string): Promise { @@ -117,9 +119,10 @@ export class AuthService { expiresIn: '15m', }); - console.log(`Reset token: ${token}`); - - // substituir por envio real de email + await this.emailService.sendResetPasswordEmail( + user.email, + token, + ); } async resetPassword(token: string, newPassword: string): Promise { diff --git a/backend/src/modules/email/email.controller.ts b/backend/src/modules/email/email.controller.ts new file mode 100644 index 0000000..1a0100b --- /dev/null +++ b/backend/src/modules/email/email.controller.ts @@ -0,0 +1,7 @@ +import { Controller } from '@nestjs/common'; +import { EmailService } from './email.service'; + +@Controller('email') +export class EmailController { + constructor(private readonly emailService: EmailService) {} +} diff --git a/backend/src/modules/email/email.module.ts b/backend/src/modules/email/email.module.ts new file mode 100644 index 0000000..36b4b26 --- /dev/null +++ b/backend/src/modules/email/email.module.ts @@ -0,0 +1,10 @@ +import { Module } from '@nestjs/common'; +import { EmailService } from './email.service'; +import { EmailController } from './email.controller'; + +@Module({ + controllers: [EmailController], + providers: [EmailService], + exports: [EmailService], +}) +export class EmailModule {} diff --git a/backend/src/modules/email/email.service.ts b/backend/src/modules/email/email.service.ts new file mode 100644 index 0000000..c85e974 --- /dev/null +++ b/backend/src/modules/email/email.service.ts @@ -0,0 +1,29 @@ +import { Injectable } from '@nestjs/common'; +import * as nodemailer from 'nodemailer'; + +@Injectable() +export class EmailService { + private transporter = nodemailer.createTransport({ + service: 'gmail', + auth: { + user: process.env.EMAIL_USER, + pass: process.env.EMAIL_PASS, + }, + }); + + async sendResetPasswordEmail(email: string, token: string) { + const resetLink = `myapp://reset-password?token=${token}`; + + await this.transporter.sendMail({ + to: email, + subject: 'Recuperação de senha', + html: ` +

Recuperação de senha

+

Clique no link abaixo:

+ Redefinir senha +

Se não abrir, copie o link:

+

${resetLink}

+ `, + }); + } +} \ No newline at end of file From 4c007a258bbeb1070ceb350656fd011060f22874 Mon Sep 17 00:00:00 2001 From: Gabriel Viell Castilho Date: Fri, 24 Apr 2026 14:05:37 -0300 Subject: [PATCH 3/3] docs: update CONTRIBUTING.md --- backend/.env.example | 5 ++++- backend/src/modules/user/dtos/createUserDTO.ts | 8 -------- 2 files changed, 4 insertions(+), 9 deletions(-) diff --git a/backend/.env.example b/backend/.env.example index b2ef93d..f68889f 100644 --- a/backend/.env.example +++ b/backend/.env.example @@ -3,4 +3,7 @@ MONGO_URI=mongodb://localhost:27017/prodesk # APP PORT=3000 -NODE_ENV=development \ No newline at end of file +NODE_ENV=development + +EMAIL_USER=seuemail@gmail.com +EMAIL_PASS=sua_senha_de_app \ No newline at end of file diff --git a/backend/src/modules/user/dtos/createUserDTO.ts b/backend/src/modules/user/dtos/createUserDTO.ts index 2a7ab94..aa9de94 100644 --- a/backend/src/modules/user/dtos/createUserDTO.ts +++ b/backend/src/modules/user/dtos/createUserDTO.ts @@ -160,12 +160,4 @@ export class CreateClientDTO { @ApiProperty({ example: '65f1a2b3c9d123456789abcd' }) @IsString() companyId: string; - - @ApiPropertyOptional({ - example: ['65f1a2b3c9d123456789abcd'], - }) - @IsOptional() - @IsArray() - @IsMongoId({ each: true }) - categories?: string[]; } \ No newline at end of file