Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Sistema de encerramento de sessão BugFix-US03 #12

Open
wants to merge 8 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from 2 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
85 changes: 82 additions & 3 deletions src/auth/auth.service.spec.ts
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Devido a mudanças a switch de teste não está rodando poderia tentar resolver e me acionar caso tenha alguma duvida ?

Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,8 @@ import * as bcrypt from 'bcryptjs';
import { repositoryMockFactory } from '../../test/database/utils';
import { UnauthorizedException } from '@nestjs/common';
import * as nodemailer from 'nodemailer';
import { ConfigService } from '@nestjs/config';
import { SignInDto } from './dtos/signIn.dto';

describe('AuthService', () => {
let service: AuthService;
Expand Down Expand Up @@ -36,15 +38,34 @@ describe('AuthService', () => {
verifyAsync: jest.fn(async () => 'refresh-token'),
},
},
{
provide: ConfigService,
useValue: {
get: jest.fn((key: string) => {
switch (key) {
case 'PASSWORD_MIN_LENGTH':
return 8;
case 'PASSWORD_REQUIRE_UPPERCASE':
return true;
case 'PASSWORD_REQUIRE_NUMBER':
return true;
case 'PASSWORD_REQUIRE_SPECIAL_CHAR':
return true;
default:
return null;
}
}),
},
},
],
}).compile();

service = module.get<AuthService>(AuthService);
userRepository = module.get<Repository<User>>(getRepositoryToken(User));
jwtService = module.get<JwtService>(JwtService);

jest.spyOn(bcrypt, 'hash').mockResolvedValueOnce('hashed-password');
jest.spyOn(bcrypt, 'genSalt').mockResolvedValueOnce(10);
jest.spyOn(bcrypt, 'hash').mockResolvedValue('hashed-password');
jest.spyOn(bcrypt, 'genSalt').mockResolvedValue(10);

sendMailMock = jest.fn();
jest.spyOn(nodemailer, 'createTransport').mockReturnValue({
Expand Down Expand Up @@ -175,6 +196,64 @@ describe('AuthService', () => {
});
});

describe('signIn with keepLoggedIn', () => {
it('should return a token with 30m expiration when keepLoggedIn is false', async () => {
const signInDto: SignInDto = {
email: '[email protected]',
password: 'password',
role: UserRoles.User,
keepLoggedIn: false,
};
const user = new User();
user.id = 'user-id';
user.email = signInDto.email;
user.password = 'hashed-password';
user.role = UserRoles.User;

jest.spyOn(userRepository, 'findOneBy').mockResolvedValue(user);
jest.spyOn(bcrypt, 'compare').mockResolvedValue(true);
const signAsyncSpy = jest.spyOn(jwtService, 'signAsync');

const result = await service.signIn(signInDto);

expect(result.accessToken).toBeDefined();

expect(signAsyncSpy).toHaveBeenNthCalledWith(
1,
{ sub: user.id, email: user.email, role: user.role },
{ expiresIn: '30m' },
);
});

it('should return a token with 7d expiration when keepLoggedIn is true', async () => {
const signInDto: SignInDto = {
email: '[email protected]',
password: 'password',
role: UserRoles.User,
keepLoggedIn: true,
};
const user = new User();
user.id = 'user-id';
user.email = signInDto.email;
user.password = 'hashed-password';
user.role = UserRoles.User;

jest.spyOn(userRepository, 'findOneBy').mockResolvedValue(user);
jest.spyOn(bcrypt, 'compare').mockResolvedValue(true);
const signAsyncSpy = jest.spyOn(jwtService, 'signAsync');

const result = await service.signIn(signInDto);

expect(result.accessToken).toBeDefined();

expect(signAsyncSpy).toHaveBeenNthCalledWith(
1,
{ sub: user.id, email: user.email, role: user.role },
{ expiresIn: '7d' },
);
});
});

describe('getProfile', () => {
it('should return the user profile when user exists', async () => {
const userId = '123';
Expand Down Expand Up @@ -245,4 +324,4 @@ describe('AuthService', () => {
expect(sendMailMock).toHaveBeenCalled();
});
});
});
});
52 changes: 47 additions & 5 deletions src/auth/auth.service.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { Injectable, UnauthorizedException } from '@nestjs/common';
import { Injectable, UnauthorizedException, BadRequestException } from '@nestjs/common';
import { JwtService } from '@nestjs/jwt';
import { InjectRepository } from '@nestjs/typeorm';
import { User, UserRoles } from '../database/entities/user.entity';
Expand All @@ -8,47 +8,85 @@ import { SignInDto } from './dtos/signIn.dto';
import { SignInResponseDto } from './dtos/signInResponse.dto';
import { SignUpDto } from './dtos/signUp.dto';
import * as nodemailer from 'nodemailer';
import { ConfigService } from '@nestjs/config';


export class InvalidPasswordException extends BadRequestException {
constructor(message: string) {
super(message);
}
}

@Injectable()
export class AuthService {
constructor(
@InjectRepository(User)
private usersRepository: Repository<User>,
private jwtService: JwtService,
private configService: ConfigService
) {}

private validatePassword(password: string): void {
const minLength = 8;
const hasUpperCase = /[A-Z]/.test(password);
const hasNumber = /[0-9]/.test(password);
const hasSpecialChar = /[!@#$%^&*(),.?":{}|<>]/.test(password);

if (password.length < minLength) {
throw new BadRequestException('A senha deve ter pelo menos 8 caracteres.');
}
if (!hasUpperCase) {
throw new BadRequestException('A senha deve conter pelo menos uma letra maiúscula.');
}
if (!hasNumber) {
throw new BadRequestException('A senha deve conter pelo menos um número.');
}
if (!hasSpecialChar) {
throw new BadRequestException('A senha deve conter pelo menos um caractere especial.');
}
}

async signIn({
email,
password,
role,
}: SignInDto): Promise<SignInResponseDto> {
keepLoggedIn = false,
}: SignInDto & { keepLoggedIn?: boolean }): Promise<SignInResponseDto> {
const user = await this.usersRepository.findOneBy({ email, role });
if (!user || !(await bcrypt.compare(password, user.password))) {
throw new UnauthorizedException('E-mail ou senha inválidos.');
}

const payload = { sub: user.id, email: user.email, role: user.role };
const accessTokenExpiresIn = keepLoggedIn ? '7d' : '30m';

return {
accessToken: await this.jwtService.signAsync(payload),
refreshToken: await this.jwtService.signAsync(payload),
accessToken: await this.jwtService.signAsync(payload, { expiresIn: accessTokenExpiresIn }),
refreshToken: await this.jwtService.signAsync(payload), // Considere adicionar uma expiração ao refreshToken também
};
}

async signUp(dto: SignUpDto): Promise<SignInResponseDto> {
// Validação da senha ANTES da criação do usuário
this.validatePassword(dto.password);

const userExists = await this.usersRepository.findOneBy({
email: dto.email,
});
if (userExists) throw new UnauthorizedException('Usuário já cadastrado.');

const user = this.usersRepository.create({
...dto,
role: UserRoles.User,
password: await bcrypt.hash(dto.password, await bcrypt.genSalt(10)),
});
await this.usersRepository.save(user);

return this.signIn({
email: dto.email,
password: dto.password,
role: user.role,
keepLoggedIn: false
});
}

Expand Down Expand Up @@ -92,8 +130,12 @@ export class AuthService {
): Promise<{ success: boolean }> {
const user = await this.usersRepository.findOneBy({ id });
if (!user) throw new UnauthorizedException('Usuário não encontrado.');

// Validação da senha ANTES de atualizar
this.validatePassword(password);

user.password = await bcrypt.hash(password, await bcrypt.genSalt(10));
await this.usersRepository.save(user);
return { success: true };
}
}
}
2 changes: 2 additions & 0 deletions src/auth/dtos/signIn.dto.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,4 +11,6 @@ export class SignInDto {

@IsNotEmpty()
role: UserRoles;

keepLoggedIn?: boolean;
}
Loading