diff --git a/src/app.module.ts b/src/app.module.ts index 9d25960..b09a08c 100644 --- a/src/app.module.ts +++ b/src/app.module.ts @@ -17,10 +17,12 @@ import { HttpModule } from '@nestjs/axios'; import { LogisticController } from './logistic/logistic.controller'; import { LogisticService } from './logistic/logistic.service'; import { LoggerModule } from './Log/logger.module'; +import { UsersModule } from './auth/users/users.module'; @Module({ imports: [ - ConfigModule.forRoot({ isGlobal: true }), // necessário para ConfigService + UsersModule, + ConfigModule.forRoot({ isGlobal: true }), TypeOrmModule.forRootAsync({ name: 'oracle', inject: [ConfigService], diff --git a/src/auth/auth/auth.controller.ts b/src/auth/auth/auth.controller.ts index d2b73d0..93c9cc3 100644 --- a/src/auth/auth/auth.controller.ts +++ b/src/auth/auth/auth.controller.ts @@ -9,10 +9,8 @@ import { } from '@nestjs/common'; import { AuthService } from './auth.service'; import { UsersService } from '../users/users.service'; -import { UserModel } from 'src/core/models/user.model'; import { ResultModel } from 'src/core/models/result.model'; -import { ResetPasswordModel } from 'src/core/models/reset-password.model'; -import { ChangePasswordModel } from 'src/core/models/change-password.model'; +import { LoginDto } from '../dto/login.dto'; @Controller('api/v1/auth') export class AuthController { @@ -22,22 +20,26 @@ export class AuthController { ) { } @Post('login') - async login(@Body() model: UserModel): Promise { - const user = await this.usersService.authenticate(model); +async login(@Body() model: LoginDto): Promise { + const user = await this.usersService.authenticate({ + userName: model.username, + password: model.password, + }); + if (!user) throw new HttpException( new ResultModel(false, 'Usuário ou senha inválidos.', null, null), HttpStatus.UNAUTHORIZED, ); - + const token = await this.authService.createToken( user.id, user.sellerId, - user.username, + user.name, user.email, user.storeId ); - + return { id: user.id, sellerId: user.sellerId, @@ -48,25 +50,4 @@ export class AuthController { token: token, }; } - - @Post('reset-password') - async resetPassword(@Body() resetPassword: ResetPasswordModel) { - const response = await this.usersService.resetPassword(resetPassword); - if (response == null) { - throw new HttpException('Usuário não foi encontrado', HttpStatus.NOT_FOUND); - } - return { message: 'Senha alterada com sucesso! Foi enviado email com a nova senha!' }; - - } - - @Post('change-password') - async changePassword(@Body() changePassword: ChangePasswordModel) { - const response = await this.usersService.changePassword(changePassword); - if (response == null) { - throw new HttpException('Usuário não foi encontrado', HttpStatus.NOT_FOUND); - } - return { message: 'Senha alterada com sucesso!' }; - - } - -} \ No newline at end of file +} \ No newline at end of file diff --git a/src/auth/auth/authenticate-user.service.ts b/src/auth/auth/authenticate-user.service.ts new file mode 100644 index 0000000..fbf5d67 --- /dev/null +++ b/src/auth/auth/authenticate-user.service.ts @@ -0,0 +1,23 @@ +import { Injectable, ForbiddenException } from '@nestjs/common'; +import { UserRepository } from '../users/UserRepository'; + +@Injectable() +export class AuthenticateUserService { + constructor(private readonly userRepository: UserRepository) {} + + async execute(username: string, password: string) { + const user = await this.userRepository.findByUsernameAndPassword(username, password); + + if (!user) return null; + + if (user.dataDesligamento !== null) { + throw new ForbiddenException('Usuário desligado da empresa, login não permitido!'); + } + + if (user.situacao === 'I') { + throw new ForbiddenException('Usuário inativo, login não permitido!'); + } + + return user; + } +} diff --git a/src/auth/dto/login.dto.ts b/src/auth/dto/login.dto.ts new file mode 100644 index 0000000..c6db1eb --- /dev/null +++ b/src/auth/dto/login.dto.ts @@ -0,0 +1,5 @@ +export class LoginDto { + username: string; + password: string; + } + \ No newline at end of file diff --git a/src/auth/models/result.ts b/src/auth/models/result.ts new file mode 100644 index 0000000..4d67e32 --- /dev/null +++ b/src/auth/models/result.ts @@ -0,0 +1,16 @@ +export class Result { + private constructor( + public readonly success: boolean, + public readonly data?: T, + public readonly error?: string, + ) {} + + static ok(data: U): Result { + return new Result(true, data); + } + + static fail(message: string): Result { + return new Result(false, undefined, message); + } + } + \ No newline at end of file diff --git a/src/auth/users/UserRepository.ts b/src/auth/users/UserRepository.ts new file mode 100644 index 0000000..a255d48 --- /dev/null +++ b/src/auth/users/UserRepository.ts @@ -0,0 +1,64 @@ +import { Injectable } from '@nestjs/common'; +import { DataSource } from 'typeorm'; +import { InjectDataSource } from '@nestjs/typeorm'; + +@Injectable() +export class UserRepository { + constructor( + @InjectDataSource('oracle') // especifica que queremos o DataSource da conexão 'oracle' + private readonly dataSource: DataSource, + ) {} + + async findByUsernameAndPassword(username: string, password: string) { + const sql = ` + SELECT + PCEMPR.MATRICULA AS "id", + PCEMPR.NOME AS "name", + PCEMPR.CODUSUR AS "sellerId", + PCEMPR.CODFILIAL AS "storeId", + PCEMPR.EMAIL AS "email", + PCEMPR.DTDEMISSAO as "dataDesligamento", + PCEMPR.SITUACAO as "situacao" + FROM PCEMPR + WHERE PCEMPR.USUARIOBD = :1 + AND PCEMPR.SENHABD = CRYPT(:2, PCEMPR.USUARIOBD) + `; + const users = await this.dataSource.query(sql, [ + username.toUpperCase(), + password.toUpperCase(), + ]); + + return users[0] || null; + } + + async findByCpfAndEmail(cpf: string, email: string) { + const sql = ` + SELECT PCUSUARI.CODUSUR as "sellerId", + PCUSUARI.NOME as "name", + PCUSUARI.EMAIL as "email" + FROM PCUSUARI + WHERE REGEXP_REPLACE(PCUSUARI.CPF, '[^0-9]', '') = REGEXP_REPLACE(:1, '[^0-9]', '') + AND PCUSUARI.EMAIL = :2 + `; + + const users = await this.dataSource.query(sql, [cpf, email]); + return users[0] || null; + } + + async updatePassword(sellerId: number, newPasswordHash: string) { + const sql = ` + UPDATE PCUSUARI SET SENHALOGIN = :1 WHERE CODUSUR = :2 + `; + await this.dataSource.query(sql, [newPasswordHash, sellerId]); + } + + async findByIdAndPassword(sellerId: number, passwordHash: string) { + const sql = ` + SELECT CODUSUR as "sellerId", NOME as "name", EMAIL as "email" + FROM PCUSUARI + WHERE CODUSUR = :1 AND SENHALOGIN = :2 + `; + const result = await this.dataSource.query(sql, [sellerId, passwordHash]); + return result[0] || null; + } +} diff --git a/src/auth/users/change-password.service.ts b/src/auth/users/change-password.service.ts new file mode 100644 index 0000000..aedfb78 --- /dev/null +++ b/src/auth/users/change-password.service.ts @@ -0,0 +1,23 @@ +import { Injectable, NotFoundException } from '@nestjs/common'; +import { UserRepository } from '../users/UserRepository'; +import md5 = require('md5'); + +@Injectable() +export class ChangePasswordService { + constructor(private readonly userRepository: UserRepository) {} + + async execute(userId: number, oldPassword: string, newPassword: string) { + const current = await this.userRepository.findByIdAndPassword( + userId, + md5(oldPassword).toUpperCase(), + ); + + if (!current) { + throw new NotFoundException('Usuário não encontrado ou senha inválida'); + } + + const newPasswordHash = md5(newPassword).toUpperCase(); + await this.userRepository.updatePassword(userId, newPasswordHash); + return current; + } +} diff --git a/src/auth/users/dto/auth-request.dto.ts b/src/auth/users/dto/auth-request.dto.ts deleted file mode 100644 index 1c569bb..0000000 --- a/src/auth/users/dto/auth-request.dto.ts +++ /dev/null @@ -1,4 +0,0 @@ -export class AuthRequestDto { - userName: string; - password: string; -} \ No newline at end of file diff --git a/src/auth/users/email.service.ts b/src/auth/users/email.service.ts new file mode 100644 index 0000000..4059496 --- /dev/null +++ b/src/auth/users/email.service.ts @@ -0,0 +1,18 @@ +import { Injectable } from '@nestjs/common'; + +@Injectable() +export class EmailService { + async sendPasswordReset(email: string, newPassword: string) { + const sql = ` + INSERT INTO CORRESPONDENCIAS ( + CORRESPONDENCIA_ID, DTINCLUSAO, TITULO, MENSAGEM, EMAIL, DESTINATARIO + ) VALUES ( + SEQ_CORRESPONDENCIAS.NEXTVAL, SYSDATE, 'Alteração de senha - CoteLivia', + 'Sua nova senha para acesso ao portal COTELIVIA é ${newPassword}', + :email, :email + ) + `; + + console.log(`[Email enviado para ${email}] Senha: ${newPassword}`); + } +} diff --git a/src/auth/users/models/change-password.model.ts b/src/auth/users/models/change-password.model.ts deleted file mode 100644 index f7b0462..0000000 --- a/src/auth/users/models/change-password.model.ts +++ /dev/null @@ -1,6 +0,0 @@ -export class ChangePasswordModel { - id: string; - currentPassword: string; - newPassword: string; - } - \ No newline at end of file diff --git a/src/auth/users/models/reset-password.model.ts b/src/auth/users/models/reset-password.model.ts deleted file mode 100644 index d311934..0000000 --- a/src/auth/users/models/reset-password.model.ts +++ /dev/null @@ -1,5 +0,0 @@ -export class ResetPasswordModel { - document: string; - email: string; - } - \ No newline at end of file diff --git a/src/auth/users/models/user.model.ts b/src/auth/users/models/user.model.ts deleted file mode 100644 index 518efd7..0000000 --- a/src/auth/users/models/user.model.ts +++ /dev/null @@ -1,10 +0,0 @@ -export interface UserModel { - id: string; - name: string; - sellerId: string; - storeId: string; - email: string; - username: string; - dataDesligamento: Date | null; - situacao: string; -} diff --git a/src/auth/users/reset-password.service.ts b/src/auth/users/reset-password.service.ts new file mode 100644 index 0000000..8296574 --- /dev/null +++ b/src/auth/users/reset-password.service.ts @@ -0,0 +1,28 @@ +import { Injectable } from '@nestjs/common'; +import { UserRepository } from './UserRepository'; +import { EmailService } from './email.service'; +import { DataSource } from 'typeorm'; +import { Guid } from 'guid-typescript'; +import * as md5 from 'md5'; +import { InjectDataSource } from '@nestjs/typeorm'; + +@Injectable() +export class ResetPasswordService { + constructor( + @InjectDataSource('oracle') private readonly dataSource: DataSource, + private readonly userRepository: UserRepository, + private readonly emailService: EmailService, + ) {} + + async execute(document: string, email: string) { + const user = await this.userRepository.findByCpfAndEmail(document, email); + if (!user) return null; + + const newPassword = Guid.create().toString().substring(0, 8); + await this.userRepository.updatePassword(user.sellerId, md5(newPassword).toUpperCase()); + + await this.emailService.sendPasswordReset(user.email, newPassword); + + return { ...user, newPassword }; + } +} diff --git a/src/auth/users/users.module.ts b/src/auth/users/users.module.ts index d66312e..d6bc6bd 100644 --- a/src/auth/users/users.module.ts +++ b/src/auth/users/users.module.ts @@ -1,10 +1,25 @@ -/* eslint-disable @typescript-eslint/no-unused-vars */ import { Module } from '@nestjs/common'; +import { TypeOrmModule } from '@nestjs/typeorm'; // <-- importe aqui import { UsersService } from './users.service'; +import { UserRepository } from './UserRepository'; +import { AuthenticateUserService } from '../auth/authenticate-user.service'; +import { ResetPasswordService } from './reset-password.service'; +import { ChangePasswordService } from './change-password.service'; +import { EmailService } from './email.service'; + @Module({ - imports: [], - providers: [UsersService], + imports: [ + TypeOrmModule.forFeature([]), + ], + providers: [ + UsersService, + UserRepository, + AuthenticateUserService, + ResetPasswordService, + ChangePasswordService, + EmailService, + ], exports: [UsersService], }) -export class UsersModule {} \ No newline at end of file +export class UsersModule {} diff --git a/src/auth/users/users.service.ts b/src/auth/users/users.service.ts index a3ab163..a641bac 100644 --- a/src/auth/users/users.service.ts +++ b/src/auth/users/users.service.ts @@ -1,151 +1,26 @@ -import { HttpException, HttpStatus, Injectable } from '@nestjs/common'; -import { DataSource } from 'typeorm'; -import md5 = require('md5'); -import { Guid } from "guid-typescript"; -import { createOracleConfig } from '../../core/configs/typeorm.oracle.config'; -import { ConfigService } from '@nestjs/config'; +import { Injectable } from '@nestjs/common'; +import { AuthenticateUserService } from '../auth/authenticate-user.service'; +import { ResetPasswordService } from './reset-password.service'; +import { ChangePasswordService } from './change-password.service'; + @Injectable() export class UsersService { - constructor(private readonly configService: ConfigService) {} + constructor( + private readonly authenticateUserService: AuthenticateUserService, + private readonly resetPasswordService: ResetPasswordService, + private readonly changePasswordService: ChangePasswordService, + ) {} - async authenticate(user: any): Promise { - const dataSource = new DataSource(createOracleConfig(this.configService)); - await dataSource.initialize(); - const queryRunner = dataSource.createQueryRunner(); - await queryRunner.connect(); - try { - const sql = `SELECT PCEMPR.MATRICULA AS "id" - ,PCEMPR.NOME AS "name" - ,PCEMPR.CODUSUR AS "sellerId" - ,PCEMPR.CODFILIAL AS "storeId" - ,PCEMPR.EMAIL AS "email" - ,PCEMPR.DTDEMISSAO as "dataDesligamento" - ,PCEMPR.SITUACAO as "situacao" - FROM PCEMPR - WHERE PCEMPR.USUARIOBD = '${user.userName}' - AND PCEMPR.SENHABD = CRYPT('${user.password.toUpperCase()}', PCEMPR.USUARIOBD) `; - - - const users = await queryRunner.manager.query(sql); - - if (users.length == 0) { - return null; - } - - const userDb = users[0]; - - if ( userDb.dataDesligamento !== null ) { - throw new HttpException('Usuário desligado da empresa, login não permitido!', HttpStatus.FORBIDDEN); - } - - if ( userDb.situacao == 'I' ) { - throw new HttpException('Usuário inativo, login não permitido!', HttpStatus.FORBIDDEN); - } - return userDb; - } finally { - await queryRunner.release(); - await dataSource.destroy(); - } + async authenticate(user: { userName: string; password: string }) { + return this.authenticateUserService.execute(user.userName, user.password); } - async resetPassword(user: any): Promise { - const dataSource = new DataSource(createOracleConfig(this.configService)); - await dataSource.initialize(); - const queryRunner = dataSource.createQueryRunner(); - await queryRunner.connect(); - await queryRunner.startTransaction(); - try { - let sql = - 'SELECT PCUSUARI.CODUSUR as "sellerId" ' + - ' ,PCUSUARI.NOME as "name" ' + - ' ,PCUSUARI.EMAIL as "email" ' + - ' FROM PCUSUARI ' + - ` WHERE REGEXP_REPLACE(PCUSUARI.CPF, '[^0-9]', '') = REGEXP_REPLACE(:1, '[^0-9]', '') ` + - ` AND PCUSUARI.EMAIL = :2 `; - - const users = await queryRunner.manager.query(sql, [ - user.document, - user.email, - ]); - - if (users.length == 0) { - return null; - } - - const guid = Guid.create(); - console.log(guid.toString()); - const password = guid.toString().substring(0, 8); - const newPassword = md5(password).toUpperCase(); - console.log("Senha:" + newPassword) - sql = `UPDATE PCUSUARI SET ` + - ` SENHALOGIN = :1 ` + - `WHERE CODUSUR = :2`; - await queryRunner.manager.query(sql, [newPassword, users[0].sellerId]); - - const sqlEmail = `INSERT INTO CORRESPONDENCIAS ( CORRESPONDENCIA_ID, DTINCLUSAO, TITULO, MENSAGEM, EMAIL, DESTINATARIO ) - VALUES ( SEQ_CORRESPONDENCIAS.NEXTVAL, SYSDATE, 'Alteração de email - CoteLivia', - 'Sua senha para acesso ao portal COTELIVIA é ${password}', '${users[0].email}', '${users[0].email}' )`; - await queryRunner.manager.query(sqlEmail); - await queryRunner.commitTransaction(); - - const userDb = users[0]; - return userDb; - } catch (error) { - await queryRunner.rollbackTransaction(); - console.log(error); - throw new Error(error); - } - finally { - await queryRunner.release(); - await dataSource.destroy(); - } + async resetPassword(user: { document: string; email: string }) { + return this.resetPasswordService.execute(user.document, user.email); } - - async changePassword(user: any): Promise { - const dataSource = new DataSource(createOracleConfig(this.configService)); - await dataSource.initialize(); - const queryRunner = dataSource.createQueryRunner(); - await queryRunner.connect(); - await queryRunner.startTransaction(); - console.log(JSON.stringify(user)); - try { - let sql = - 'SELECT PCUSUARI.CODUSUR as "sellerId" ' + - ' ,PCUSUARI.NOME as "name" ' + - ' ,PCUSUARI.EMAIL as "email" ' + - ' FROM PCUSUARI ' + - ` WHERE PCUSUARI.CODUSUR = :1` + - ` AND PCUSUARI.SENHALOGIN = :2 `; - - const users = await queryRunner.manager.query(sql, [ - user.id, - md5(user.password).toUpperCase(), - ]); - - if (users.length == 0) { - return null; - } - - sql = `UPDATE PCUSUARI SET ` + - ` SENHALOGIN = :1 ` + - `WHERE CODUSUR = :2`; - await queryRunner.manager.query(sql, [md5(user.newPassword).toUpperCase(), users[0].sellerId]); - - await queryRunner.commitTransaction(); - - const userDb = users[0]; - return userDb; - } catch (error) { - await queryRunner.rollbackTransaction(); - console.log(error); - throw new Error(error); - } - finally { - await queryRunner.release(); - await dataSource.destroy(); - } + async changePassword(user: { id: number; password: string; newPassword: string }) { + return this.changePasswordService.execute(user.id, user.password, user.newPassword); } - -} \ No newline at end of file +}