refactor: atualizações e remoção de módulos não utilizados
This commit is contained in:
@@ -46,7 +46,9 @@ export interface AuthServiceTestContext {
|
||||
mockUserRepository: ReturnType<typeof createMockUserRepository>;
|
||||
mockTokenBlacklistService: ReturnType<typeof createMockTokenBlacklistService>;
|
||||
mockRefreshTokenService: ReturnType<typeof createMockRefreshTokenService>;
|
||||
mockSessionManagementService: ReturnType<typeof createMockSessionManagementService>;
|
||||
mockSessionManagementService: ReturnType<
|
||||
typeof createMockSessionManagementService
|
||||
>;
|
||||
}
|
||||
|
||||
export async function createAuthServiceTestModule(): Promise<AuthServiceTestContext> {
|
||||
@@ -101,4 +103,3 @@ export async function createAuthServiceTestModule(): Promise<AuthServiceTestCont
|
||||
mockSessionManagementService,
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@@ -29,7 +29,7 @@ describe('AuthService - createToken', () => {
|
||||
username,
|
||||
email,
|
||||
storeId,
|
||||
sessionId
|
||||
sessionId,
|
||||
);
|
||||
|
||||
expect(context.mockJwtService.sign).toHaveBeenCalledWith(
|
||||
@@ -41,7 +41,7 @@ describe('AuthService - createToken', () => {
|
||||
email: email,
|
||||
sessionId: sessionId,
|
||||
},
|
||||
{ expiresIn: '8h' }
|
||||
{ expiresIn: '8h' },
|
||||
);
|
||||
expect(result).toBe(mockToken);
|
||||
});
|
||||
@@ -61,7 +61,7 @@ describe('AuthService - createToken', () => {
|
||||
sellerId,
|
||||
username,
|
||||
email,
|
||||
storeId
|
||||
storeId,
|
||||
);
|
||||
|
||||
expect(context.mockJwtService.sign).toHaveBeenCalledWith(
|
||||
@@ -73,7 +73,7 @@ describe('AuthService - createToken', () => {
|
||||
email: email,
|
||||
sessionId: undefined,
|
||||
},
|
||||
{ expiresIn: '8h' }
|
||||
{ expiresIn: '8h' },
|
||||
);
|
||||
expect(result).toBe(mockToken);
|
||||
});
|
||||
@@ -93,12 +93,12 @@ describe('AuthService - createToken', () => {
|
||||
sellerId,
|
||||
username,
|
||||
email,
|
||||
storeId
|
||||
storeId,
|
||||
);
|
||||
|
||||
expect(context.mockJwtService.sign).toHaveBeenCalledWith(
|
||||
expect.any(Object),
|
||||
{ expiresIn: '8h' }
|
||||
{ expiresIn: '8h' },
|
||||
);
|
||||
});
|
||||
|
||||
@@ -119,7 +119,7 @@ describe('AuthService - createToken', () => {
|
||||
username,
|
||||
email,
|
||||
storeId,
|
||||
sessionId
|
||||
sessionId,
|
||||
);
|
||||
|
||||
const signCall = context.mockJwtService.sign.mock.calls[0];
|
||||
@@ -150,7 +150,7 @@ describe('AuthService - createToken', () => {
|
||||
username,
|
||||
email,
|
||||
storeId,
|
||||
sessionId
|
||||
sessionId,
|
||||
);
|
||||
|
||||
expect(context.mockJwtService.sign).toHaveBeenCalledWith(
|
||||
@@ -162,7 +162,7 @@ describe('AuthService - createToken', () => {
|
||||
email: email,
|
||||
sessionId: sessionId,
|
||||
},
|
||||
{ expiresIn: '8h' }
|
||||
{ expiresIn: '8h' },
|
||||
);
|
||||
expect(result).toBe(mockToken);
|
||||
});
|
||||
@@ -171,7 +171,13 @@ describe('AuthService - createToken', () => {
|
||||
const mockToken = 'mock.jwt.token.once';
|
||||
context.mockJwtService.sign.mockReturnValue(mockToken);
|
||||
|
||||
await context.service.createToken(1, 100, 'test', 'test@test.com', 'STORE001');
|
||||
await context.service.createToken(
|
||||
1,
|
||||
100,
|
||||
'test',
|
||||
'test@test.com',
|
||||
'STORE001',
|
||||
);
|
||||
|
||||
expect(context.mockJwtService.sign).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
@@ -183,7 +189,7 @@ describe('AuthService - createToken', () => {
|
||||
* de validação no método createToken. Atualmente, o método não valida
|
||||
* os parâmetros de entrada, o que pode causar problemas de segurança
|
||||
* e tokens inválidos.
|
||||
*
|
||||
*
|
||||
* PROBLEMAS IDENTIFICADOS:
|
||||
* 1. Não valida se IDs são positivos
|
||||
* 2. Não valida se strings estão vazias
|
||||
@@ -199,7 +205,13 @@ describe('AuthService - createToken', () => {
|
||||
const negativeId = -1;
|
||||
|
||||
await expect(
|
||||
context.service.createToken(negativeId, 100, 'test', 'test@test.com', 'STORE001')
|
||||
context.service.createToken(
|
||||
negativeId,
|
||||
100,
|
||||
'test',
|
||||
'test@test.com',
|
||||
'STORE001',
|
||||
),
|
||||
).rejects.toThrow('ID de usuário inválido');
|
||||
});
|
||||
|
||||
@@ -207,7 +219,13 @@ describe('AuthService - createToken', () => {
|
||||
const zeroId = 0;
|
||||
|
||||
await expect(
|
||||
context.service.createToken(zeroId, 100, 'test', 'test@test.com', 'STORE001')
|
||||
context.service.createToken(
|
||||
zeroId,
|
||||
100,
|
||||
'test',
|
||||
'test@test.com',
|
||||
'STORE001',
|
||||
),
|
||||
).rejects.toThrow('ID de usuário inválido');
|
||||
});
|
||||
|
||||
@@ -215,7 +233,13 @@ describe('AuthService - createToken', () => {
|
||||
const negativeSellerId = -1;
|
||||
|
||||
await expect(
|
||||
context.service.createToken(1, negativeSellerId, 'test', 'test@test.com', 'STORE001')
|
||||
context.service.createToken(
|
||||
1,
|
||||
negativeSellerId,
|
||||
'test',
|
||||
'test@test.com',
|
||||
'STORE001',
|
||||
),
|
||||
).rejects.toThrow('ID de vendedor inválido');
|
||||
});
|
||||
|
||||
@@ -223,7 +247,13 @@ describe('AuthService - createToken', () => {
|
||||
const emptyUsername = '';
|
||||
|
||||
await expect(
|
||||
context.service.createToken(1, 100, emptyUsername, 'test@test.com', 'STORE001')
|
||||
context.service.createToken(
|
||||
1,
|
||||
100,
|
||||
emptyUsername,
|
||||
'test@test.com',
|
||||
'STORE001',
|
||||
),
|
||||
).rejects.toThrow('Nome de usuário não pode estar vazio');
|
||||
});
|
||||
|
||||
@@ -231,7 +261,13 @@ describe('AuthService - createToken', () => {
|
||||
const whitespaceUsername = ' ';
|
||||
|
||||
await expect(
|
||||
context.service.createToken(1, 100, whitespaceUsername, 'test@test.com', 'STORE001')
|
||||
context.service.createToken(
|
||||
1,
|
||||
100,
|
||||
whitespaceUsername,
|
||||
'test@test.com',
|
||||
'STORE001',
|
||||
),
|
||||
).rejects.toThrow('Nome de usuário não pode estar vazio');
|
||||
});
|
||||
|
||||
@@ -239,7 +275,7 @@ describe('AuthService - createToken', () => {
|
||||
const emptyEmail = '';
|
||||
|
||||
await expect(
|
||||
context.service.createToken(1, 100, 'test', emptyEmail, 'STORE001')
|
||||
context.service.createToken(1, 100, 'test', emptyEmail, 'STORE001'),
|
||||
).rejects.toThrow('Email não pode estar vazio');
|
||||
});
|
||||
|
||||
@@ -247,7 +283,7 @@ describe('AuthService - createToken', () => {
|
||||
const invalidEmail = 'not-an-email';
|
||||
|
||||
await expect(
|
||||
context.service.createToken(1, 100, 'test', invalidEmail, 'STORE001')
|
||||
context.service.createToken(1, 100, 'test', invalidEmail, 'STORE001'),
|
||||
).rejects.toThrow('Formato de email inválido');
|
||||
});
|
||||
|
||||
@@ -255,7 +291,7 @@ describe('AuthService - createToken', () => {
|
||||
const invalidEmail = 'testemail.com';
|
||||
|
||||
await expect(
|
||||
context.service.createToken(1, 100, 'test', invalidEmail, 'STORE001')
|
||||
context.service.createToken(1, 100, 'test', invalidEmail, 'STORE001'),
|
||||
).rejects.toThrow('Formato de email inválido');
|
||||
});
|
||||
|
||||
@@ -263,19 +299,37 @@ describe('AuthService - createToken', () => {
|
||||
const emptyStoreId = '';
|
||||
|
||||
await expect(
|
||||
context.service.createToken(1, 100, 'test', 'test@test.com', emptyStoreId)
|
||||
context.service.createToken(
|
||||
1,
|
||||
100,
|
||||
'test',
|
||||
'test@test.com',
|
||||
emptyStoreId,
|
||||
),
|
||||
).rejects.toThrow('ID da loja não pode estar vazio');
|
||||
});
|
||||
|
||||
it('should reject null username', async () => {
|
||||
await expect(
|
||||
context.service.createToken(1, 100, null as any, 'test@test.com', 'STORE001')
|
||||
context.service.createToken(
|
||||
1,
|
||||
100,
|
||||
null as any,
|
||||
'test@test.com',
|
||||
'STORE001',
|
||||
),
|
||||
).rejects.toThrow('Nome de usuário não pode estar vazio');
|
||||
});
|
||||
|
||||
it('should reject undefined email', async () => {
|
||||
await expect(
|
||||
context.service.createToken(1, 100, 'test', undefined as any, 'STORE001')
|
||||
context.service.createToken(
|
||||
1,
|
||||
100,
|
||||
'test',
|
||||
undefined as any,
|
||||
'STORE001',
|
||||
),
|
||||
).rejects.toThrow('Email não pode estar vazio');
|
||||
});
|
||||
|
||||
@@ -283,7 +337,13 @@ describe('AuthService - createToken', () => {
|
||||
const specialCharsOnly = '@#$%';
|
||||
|
||||
await expect(
|
||||
context.service.createToken(1, 100, specialCharsOnly, 'test@test.com', 'STORE001')
|
||||
context.service.createToken(
|
||||
1,
|
||||
100,
|
||||
specialCharsOnly,
|
||||
'test@test.com',
|
||||
'STORE001',
|
||||
),
|
||||
).rejects.toThrow('Nome de usuário inválido');
|
||||
});
|
||||
|
||||
@@ -291,7 +351,13 @@ describe('AuthService - createToken', () => {
|
||||
const longUsername = 'a'.repeat(10000);
|
||||
|
||||
await expect(
|
||||
context.service.createToken(1, 100, longUsername, 'test@test.com', 'STORE001')
|
||||
context.service.createToken(
|
||||
1,
|
||||
100,
|
||||
longUsername,
|
||||
'test@test.com',
|
||||
'STORE001',
|
||||
),
|
||||
).rejects.toThrow('Nome de usuário muito longo');
|
||||
});
|
||||
|
||||
@@ -299,7 +365,7 @@ describe('AuthService - createToken', () => {
|
||||
const longEmail = 'a'.repeat(10000) + '@test.com';
|
||||
|
||||
await expect(
|
||||
context.service.createToken(1, 100, 'test', longEmail, 'STORE001')
|
||||
context.service.createToken(1, 100, 'test', longEmail, 'STORE001'),
|
||||
).rejects.toThrow('Email muito longo');
|
||||
});
|
||||
|
||||
@@ -307,7 +373,13 @@ describe('AuthService - createToken', () => {
|
||||
const sqlInjection = "admin'; DROP TABLE users; --";
|
||||
|
||||
await expect(
|
||||
context.service.createToken(1, 100, sqlInjection, 'test@test.com', 'STORE001')
|
||||
context.service.createToken(
|
||||
1,
|
||||
100,
|
||||
sqlInjection,
|
||||
'test@test.com',
|
||||
'STORE001',
|
||||
),
|
||||
).rejects.toThrow('Nome de usuário contém caracteres inválidos');
|
||||
});
|
||||
|
||||
@@ -315,9 +387,8 @@ describe('AuthService - createToken', () => {
|
||||
const invalidEmail = 'test@@example.com';
|
||||
|
||||
await expect(
|
||||
context.service.createToken(1, 100, 'test', invalidEmail, 'STORE001')
|
||||
context.service.createToken(1, 100, 'test', invalidEmail, 'STORE001'),
|
||||
).rejects.toThrow('Formato de email inválido');
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
@@ -14,7 +14,7 @@ describe('AuthService - createTokenPair', () => {
|
||||
describe('createTokenPair - Tests that expose problems', () => {
|
||||
/**
|
||||
* NOTA: Estes testes identificam problemas no método createTokenPair.
|
||||
*
|
||||
*
|
||||
* PROBLEMAS IDENTIFICADOS:
|
||||
* 1. Não há rollback se um token é criado mas o outro falha
|
||||
* 2. Não valida se os tokens foram realmente gerados
|
||||
@@ -25,7 +25,9 @@ describe('AuthService - createTokenPair', () => {
|
||||
|
||||
beforeEach(() => {
|
||||
context.mockJwtService.sign.mockReturnValue('mock.access.token');
|
||||
context.mockRefreshTokenService.generateRefreshToken.mockResolvedValue('mock.refresh.token');
|
||||
context.mockRefreshTokenService.generateRefreshToken.mockResolvedValue(
|
||||
'mock.refresh.token',
|
||||
);
|
||||
});
|
||||
|
||||
it('should handle error when createToken fails after refresh token is generated', async () => {
|
||||
@@ -39,10 +41,19 @@ describe('AuthService - createTokenPair', () => {
|
||||
});
|
||||
|
||||
await expect(
|
||||
context.service.createTokenPair(1, 100, 'test', 'test@test.com', 'STORE001', 'session-123')
|
||||
context.service.createTokenPair(
|
||||
1,
|
||||
100,
|
||||
'test',
|
||||
'test@test.com',
|
||||
'STORE001',
|
||||
'session-123',
|
||||
),
|
||||
).rejects.toThrow();
|
||||
|
||||
expect(context.mockRefreshTokenService.generateRefreshToken).not.toHaveBeenCalled();
|
||||
expect(
|
||||
context.mockRefreshTokenService.generateRefreshToken,
|
||||
).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should rollback access token if refresh token generation fails', async () => {
|
||||
@@ -52,11 +63,18 @@ describe('AuthService - createTokenPair', () => {
|
||||
* Solução esperada: Invalidar o access token ou garantir atomicidade.
|
||||
*/
|
||||
context.mockRefreshTokenService.generateRefreshToken.mockRejectedValueOnce(
|
||||
new Error('Falha ao gerar refresh token')
|
||||
new Error('Falha ao gerar refresh token'),
|
||||
);
|
||||
|
||||
await expect(
|
||||
context.service.createTokenPair(1, 100, 'test', 'test@test.com', 'STORE001', 'session-123')
|
||||
context.service.createTokenPair(
|
||||
1,
|
||||
100,
|
||||
'test',
|
||||
'test@test.com',
|
||||
'STORE001',
|
||||
'session-123',
|
||||
),
|
||||
).rejects.toThrow('Falha ao gerar refresh token');
|
||||
});
|
||||
|
||||
@@ -69,7 +87,13 @@ describe('AuthService - createTokenPair', () => {
|
||||
context.mockJwtService.sign.mockReturnValue('');
|
||||
|
||||
await expect(
|
||||
context.service.createTokenPair(1, 100, 'test', 'test@test.com', 'STORE001')
|
||||
context.service.createTokenPair(
|
||||
1,
|
||||
100,
|
||||
'test',
|
||||
'test@test.com',
|
||||
'STORE001',
|
||||
),
|
||||
).rejects.toThrow('Token de acesso inválido gerado');
|
||||
});
|
||||
|
||||
@@ -79,18 +103,34 @@ describe('AuthService - createTokenPair', () => {
|
||||
* Problema: Método não valida o retorno.
|
||||
* Solução esperada: Lançar exceção se token for inválido.
|
||||
*/
|
||||
context.mockRefreshTokenService.generateRefreshToken.mockResolvedValue('');
|
||||
context.mockRefreshTokenService.generateRefreshToken.mockResolvedValue(
|
||||
'',
|
||||
);
|
||||
|
||||
await expect(
|
||||
context.service.createTokenPair(1, 100, 'test', 'test@test.com', 'STORE001')
|
||||
context.service.createTokenPair(
|
||||
1,
|
||||
100,
|
||||
'test',
|
||||
'test@test.com',
|
||||
'STORE001',
|
||||
),
|
||||
).rejects.toThrow('Refresh token inválido gerado');
|
||||
});
|
||||
|
||||
it('should validate that refresh token is not null', async () => {
|
||||
context.mockRefreshTokenService.generateRefreshToken.mockResolvedValue(null);
|
||||
context.mockRefreshTokenService.generateRefreshToken.mockResolvedValue(
|
||||
null,
|
||||
);
|
||||
|
||||
await expect(
|
||||
context.service.createTokenPair(1, 100, 'test', 'test@test.com', 'STORE001')
|
||||
context.service.createTokenPair(
|
||||
1,
|
||||
100,
|
||||
'test',
|
||||
'test@test.com',
|
||||
'STORE001',
|
||||
),
|
||||
).rejects.toThrow('Refresh token inválido gerado');
|
||||
});
|
||||
|
||||
@@ -101,18 +141,26 @@ describe('AuthService - createTokenPair', () => {
|
||||
* Solução esperada: Access token sempre primeiro.
|
||||
*/
|
||||
const callOrder = [];
|
||||
|
||||
|
||||
context.mockJwtService.sign.mockImplementation(() => {
|
||||
callOrder.push('accessToken');
|
||||
return 'mock.access.token';
|
||||
});
|
||||
|
||||
context.mockRefreshTokenService.generateRefreshToken.mockImplementation(async () => {
|
||||
callOrder.push('refreshToken');
|
||||
return 'mock.refresh.token';
|
||||
});
|
||||
|
||||
await context.service.createTokenPair(1, 100, 'test', 'test@test.com', 'STORE001');
|
||||
context.mockRefreshTokenService.generateRefreshToken.mockImplementation(
|
||||
async () => {
|
||||
callOrder.push('refreshToken');
|
||||
return 'mock.refresh.token';
|
||||
},
|
||||
);
|
||||
|
||||
await context.service.createTokenPair(
|
||||
1,
|
||||
100,
|
||||
'test',
|
||||
'test@test.com',
|
||||
'STORE001',
|
||||
);
|
||||
|
||||
expect(callOrder).toEqual(['accessToken', 'refreshToken']);
|
||||
});
|
||||
@@ -123,7 +171,13 @@ describe('AuthService - createTokenPair', () => {
|
||||
* Problema: Cliente pode não saber quando renovar o token.
|
||||
* Solução esperada: Sempre retornar um número positivo válido.
|
||||
*/
|
||||
const result = await context.service.createTokenPair(1, 100, 'test', 'test@test.com', 'STORE001');
|
||||
const result = await context.service.createTokenPair(
|
||||
1,
|
||||
100,
|
||||
'test',
|
||||
'test@test.com',
|
||||
'STORE001',
|
||||
);
|
||||
|
||||
expect(result.expiresIn).toBeGreaterThan(0);
|
||||
expect(typeof result.expiresIn).toBe('number');
|
||||
@@ -135,7 +189,7 @@ describe('AuthService - createTokenPair', () => {
|
||||
* Cenário: Múltiplas chamadas simultâneas para o mesmo usuário.
|
||||
* Problema: Pode criar múltiplos pares de tokens inconsistentes.
|
||||
* Solução esperada: JWT service gera tokens únicos com timestamps diferentes.
|
||||
*
|
||||
*
|
||||
* Nota: Na implementação real, o JWT service inclui timestamp e outros dados
|
||||
* que garantem unicidade. Aqui simulamos isso no mock.
|
||||
*/
|
||||
@@ -145,19 +199,42 @@ describe('AuthService - createTokenPair', () => {
|
||||
return `mock.access.token.${callCount}`;
|
||||
});
|
||||
|
||||
context.mockRefreshTokenService.generateRefreshToken.mockImplementation(async () => {
|
||||
return `mock.refresh.token.${Math.random()}`;
|
||||
});
|
||||
context.mockRefreshTokenService.generateRefreshToken.mockImplementation(
|
||||
async () => {
|
||||
return `mock.refresh.token.${Math.random()}`;
|
||||
},
|
||||
);
|
||||
|
||||
const promises = [
|
||||
context.service.createTokenPair(1, 100, 'test', 'test@test.com', 'STORE001', 'session-1'),
|
||||
context.service.createTokenPair(1, 100, 'test', 'test@test.com', 'STORE001', 'session-2'),
|
||||
context.service.createTokenPair(1, 100, 'test', 'test@test.com', 'STORE001', 'session-3'),
|
||||
context.service.createTokenPair(
|
||||
1,
|
||||
100,
|
||||
'test',
|
||||
'test@test.com',
|
||||
'STORE001',
|
||||
'session-1',
|
||||
),
|
||||
context.service.createTokenPair(
|
||||
1,
|
||||
100,
|
||||
'test',
|
||||
'test@test.com',
|
||||
'STORE001',
|
||||
'session-2',
|
||||
),
|
||||
context.service.createTokenPair(
|
||||
1,
|
||||
100,
|
||||
'test',
|
||||
'test@test.com',
|
||||
'STORE001',
|
||||
'session-3',
|
||||
),
|
||||
];
|
||||
|
||||
const results = await Promise.all(promises);
|
||||
|
||||
const uniqueTokens = new Set(results.map(r => r.accessToken));
|
||||
const uniqueTokens = new Set(results.map((r) => r.accessToken));
|
||||
expect(uniqueTokens.size).toBe(3);
|
||||
});
|
||||
|
||||
@@ -168,10 +245,18 @@ describe('AuthService - createTokenPair', () => {
|
||||
* Solução esperada: Falhar rápido com mensagem clara.
|
||||
*/
|
||||
await expect(
|
||||
context.service.createTokenPair(-1, 100, 'test', 'test@test.com', 'STORE001')
|
||||
context.service.createTokenPair(
|
||||
-1,
|
||||
100,
|
||||
'test',
|
||||
'test@test.com',
|
||||
'STORE001',
|
||||
),
|
||||
).rejects.toThrow('ID de usuário inválido');
|
||||
|
||||
expect(context.mockRefreshTokenService.generateRefreshToken).not.toHaveBeenCalled();
|
||||
expect(
|
||||
context.mockRefreshTokenService.generateRefreshToken,
|
||||
).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should not create refresh token if validation fails', async () => {
|
||||
@@ -181,11 +266,19 @@ describe('AuthService - createTokenPair', () => {
|
||||
* Solução esperada: Validar tudo antes de criar qualquer token.
|
||||
*/
|
||||
await expect(
|
||||
context.service.createTokenPair(1, -1, 'test', 'test@test.com', 'STORE001')
|
||||
context.service.createTokenPair(
|
||||
1,
|
||||
-1,
|
||||
'test',
|
||||
'test@test.com',
|
||||
'STORE001',
|
||||
),
|
||||
).rejects.toThrow('ID de vendedor inválido');
|
||||
|
||||
expect(context.mockJwtService.sign).not.toHaveBeenCalled();
|
||||
expect(context.mockRefreshTokenService.generateRefreshToken).not.toHaveBeenCalled();
|
||||
expect(
|
||||
context.mockRefreshTokenService.generateRefreshToken,
|
||||
).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should handle undefined sessionId gracefully', async () => {
|
||||
@@ -194,11 +287,19 @@ describe('AuthService - createTokenPair', () => {
|
||||
* Problema: Pode causar problemas ao gerar tokens sem session.
|
||||
* Solução esperada: Aceitar undefined e passar corretamente aos serviços.
|
||||
*/
|
||||
const result = await context.service.createTokenPair(1, 100, 'test', 'test@test.com', 'STORE001');
|
||||
const result = await context.service.createTokenPair(
|
||||
1,
|
||||
100,
|
||||
'test',
|
||||
'test@test.com',
|
||||
'STORE001',
|
||||
);
|
||||
|
||||
expect(result.accessToken).toBeDefined();
|
||||
expect(result.refreshToken).toBeDefined();
|
||||
expect(context.mockRefreshTokenService.generateRefreshToken).toHaveBeenCalledWith(1, undefined);
|
||||
expect(
|
||||
context.mockRefreshTokenService.generateRefreshToken,
|
||||
).toHaveBeenCalledWith(1, undefined);
|
||||
});
|
||||
|
||||
it('should include all required fields in return object', async () => {
|
||||
@@ -207,7 +308,13 @@ describe('AuthService - createTokenPair', () => {
|
||||
* Problema: Pode faltar campos ou ter campos extras.
|
||||
* Solução esperada: Sempre retornar accessToken, refreshToken e expiresIn.
|
||||
*/
|
||||
const result = await context.service.createTokenPair(1, 100, 'test', 'test@test.com', 'STORE001');
|
||||
const result = await context.service.createTokenPair(
|
||||
1,
|
||||
100,
|
||||
'test',
|
||||
'test@test.com',
|
||||
'STORE001',
|
||||
);
|
||||
|
||||
expect(result).toHaveProperty('accessToken');
|
||||
expect(result).toHaveProperty('refreshToken');
|
||||
@@ -216,4 +323,3 @@ describe('AuthService - createTokenPair', () => {
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
@@ -13,8 +13,12 @@ describe('AuthService - logout', () => {
|
||||
storeId: 'STORE001',
|
||||
sessionId: 'session-123',
|
||||
});
|
||||
context.mockTokenBlacklistService.addToBlacklist.mockResolvedValue(undefined);
|
||||
context.mockSessionManagementService.terminateSession.mockResolvedValue(undefined);
|
||||
context.mockTokenBlacklistService.addToBlacklist.mockResolvedValue(
|
||||
undefined,
|
||||
);
|
||||
context.mockSessionManagementService.terminateSession.mockResolvedValue(
|
||||
undefined,
|
||||
);
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
@@ -24,7 +28,7 @@ describe('AuthService - logout', () => {
|
||||
describe('logout - Tests that expose problems', () => {
|
||||
/**
|
||||
* NOTA: Estes testes identificam problemas no método logout.
|
||||
*
|
||||
*
|
||||
* PROBLEMAS IDENTIFICADOS:
|
||||
* 1. Não valida token de entrada (vazio, null, undefined)
|
||||
* 2. Não valida se token foi decodificado corretamente
|
||||
@@ -37,66 +41,76 @@ describe('AuthService - logout', () => {
|
||||
*/
|
||||
|
||||
it('should reject empty token', async () => {
|
||||
await expect(
|
||||
context.service.logout('')
|
||||
).rejects.toThrow('Token não pode estar vazio');
|
||||
await expect(context.service.logout('')).rejects.toThrow(
|
||||
'Token não pode estar vazio',
|
||||
);
|
||||
|
||||
expect(context.mockJwtService.decode).not.toHaveBeenCalled();
|
||||
expect(context.mockTokenBlacklistService.addToBlacklist).not.toHaveBeenCalled();
|
||||
expect(
|
||||
context.mockTokenBlacklistService.addToBlacklist,
|
||||
).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should reject null token', async () => {
|
||||
await expect(
|
||||
context.service.logout(null as any)
|
||||
).rejects.toThrow('Token não pode estar vazio');
|
||||
await expect(context.service.logout(null as any)).rejects.toThrow(
|
||||
'Token não pode estar vazio',
|
||||
);
|
||||
|
||||
expect(context.mockJwtService.decode).not.toHaveBeenCalled();
|
||||
expect(context.mockTokenBlacklistService.addToBlacklist).not.toHaveBeenCalled();
|
||||
expect(
|
||||
context.mockTokenBlacklistService.addToBlacklist,
|
||||
).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should reject undefined token', async () => {
|
||||
await expect(
|
||||
context.service.logout(undefined as any)
|
||||
).rejects.toThrow('Token não pode estar vazio');
|
||||
await expect(context.service.logout(undefined as any)).rejects.toThrow(
|
||||
'Token não pode estar vazio',
|
||||
);
|
||||
|
||||
expect(context.mockJwtService.decode).not.toHaveBeenCalled();
|
||||
expect(context.mockTokenBlacklistService.addToBlacklist).not.toHaveBeenCalled();
|
||||
expect(
|
||||
context.mockTokenBlacklistService.addToBlacklist,
|
||||
).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should reject whitespace-only token', async () => {
|
||||
await expect(
|
||||
context.service.logout(' ')
|
||||
).rejects.toThrow('Token não pode estar vazio');
|
||||
await expect(context.service.logout(' ')).rejects.toThrow(
|
||||
'Token não pode estar vazio',
|
||||
);
|
||||
|
||||
expect(context.mockJwtService.decode).not.toHaveBeenCalled();
|
||||
expect(context.mockTokenBlacklistService.addToBlacklist).not.toHaveBeenCalled();
|
||||
expect(
|
||||
context.mockTokenBlacklistService.addToBlacklist,
|
||||
).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should reject extremely long tokens (DoS prevention)', async () => {
|
||||
const hugeToken = 'a'.repeat(100000);
|
||||
|
||||
await expect(
|
||||
context.service.logout(hugeToken)
|
||||
).rejects.toThrow('Token muito longo');
|
||||
await expect(context.service.logout(hugeToken)).rejects.toThrow(
|
||||
'Token muito longo',
|
||||
);
|
||||
|
||||
expect(context.mockJwtService.decode).not.toHaveBeenCalled();
|
||||
expect(context.mockTokenBlacklistService.addToBlacklist).not.toHaveBeenCalled();
|
||||
expect(
|
||||
context.mockTokenBlacklistService.addToBlacklist,
|
||||
).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should validate decoded token is not null', async () => {
|
||||
context.mockJwtService.decode.mockReturnValue(null);
|
||||
|
||||
await expect(
|
||||
context.service.logout('invalid.token')
|
||||
).rejects.toThrow('Token inválido ou não pode ser decodificado');
|
||||
await expect(context.service.logout('invalid.token')).rejects.toThrow(
|
||||
'Token inválido ou não pode ser decodificado',
|
||||
);
|
||||
});
|
||||
|
||||
it('should validate decoded token has required fields', async () => {
|
||||
context.mockJwtService.decode.mockReturnValue({} as any);
|
||||
|
||||
await expect(
|
||||
context.service.logout('incomplete.token')
|
||||
).rejects.toThrow('Token inválido ou não pode ser decodificado');
|
||||
await expect(context.service.logout('incomplete.token')).rejects.toThrow(
|
||||
'Token inválido ou não pode ser decodificado',
|
||||
);
|
||||
});
|
||||
|
||||
it('should not add token to blacklist if already blacklisted', async () => {
|
||||
@@ -104,7 +118,9 @@ describe('AuthService - logout', () => {
|
||||
|
||||
await context.service.logout('already.blacklisted.token');
|
||||
|
||||
expect(context.mockTokenBlacklistService.addToBlacklist).not.toHaveBeenCalled();
|
||||
expect(
|
||||
context.mockTokenBlacklistService.addToBlacklist,
|
||||
).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should validate session exists before terminating', async () => {
|
||||
@@ -114,11 +130,11 @@ describe('AuthService - logout', () => {
|
||||
} as any);
|
||||
|
||||
context.mockSessionManagementService.terminateSession.mockRejectedValue(
|
||||
new Error('Sessão não encontrada')
|
||||
new Error('Sessão não encontrada'),
|
||||
);
|
||||
|
||||
await expect(
|
||||
context.service.logout('token.with.invalid.session')
|
||||
context.service.logout('token.with.invalid.session'),
|
||||
).rejects.toThrow('Sessão não encontrada');
|
||||
});
|
||||
|
||||
@@ -128,16 +144,16 @@ describe('AuthService - logout', () => {
|
||||
});
|
||||
|
||||
await expect(
|
||||
context.service.logout('invalid.token.format')
|
||||
context.service.logout('invalid.token.format'),
|
||||
).rejects.toThrow('Token inválido ou não pode ser decodificado');
|
||||
});
|
||||
|
||||
it('should sanitize token input', async () => {
|
||||
const maliciousToken = "'; DROP TABLE users; --";
|
||||
|
||||
await expect(
|
||||
context.service.logout(maliciousToken)
|
||||
).rejects.toThrow('Formato de token inválido');
|
||||
await expect(context.service.logout(maliciousToken)).rejects.toThrow(
|
||||
'Formato de token inválido',
|
||||
);
|
||||
|
||||
expect(context.mockJwtService.decode).not.toHaveBeenCalled();
|
||||
});
|
||||
@@ -149,7 +165,7 @@ describe('AuthService - logout', () => {
|
||||
} as any);
|
||||
|
||||
await expect(
|
||||
context.service.logout('token.with.invalid.id')
|
||||
context.service.logout('token.with.invalid.id'),
|
||||
).rejects.toThrow('ID de usuário inválido no token');
|
||||
});
|
||||
|
||||
@@ -161,7 +177,9 @@ describe('AuthService - logout', () => {
|
||||
|
||||
await context.service.logout('token.with.empty.sessionid');
|
||||
|
||||
expect(context.mockSessionManagementService.terminateSession).not.toHaveBeenCalled();
|
||||
expect(
|
||||
context.mockSessionManagementService.terminateSession,
|
||||
).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should complete logout even if session termination fails', async () => {
|
||||
@@ -172,23 +190,27 @@ describe('AuthService - logout', () => {
|
||||
|
||||
context.mockTokenBlacklistService.isBlacklisted.mockResolvedValue(false);
|
||||
context.mockSessionManagementService.terminateSession.mockRejectedValue(
|
||||
new Error('Falha ao terminar sessão')
|
||||
new Error('Falha ao terminar sessão'),
|
||||
);
|
||||
|
||||
await context.service.logout('valid.token');
|
||||
|
||||
expect(context.mockTokenBlacklistService.addToBlacklist).toHaveBeenCalledWith('valid.token');
|
||||
expect(
|
||||
context.mockTokenBlacklistService.addToBlacklist,
|
||||
).toHaveBeenCalledWith('valid.token');
|
||||
});
|
||||
|
||||
it('should not throw if token is already blacklisted', async () => {
|
||||
context.mockTokenBlacklistService.isBlacklisted.mockResolvedValue(true);
|
||||
context.mockTokenBlacklistService.addToBlacklist.mockRejectedValue(
|
||||
new Error('Token já está na blacklist')
|
||||
new Error('Token já está na blacklist'),
|
||||
);
|
||||
|
||||
await context.service.logout('already.blacklisted.token');
|
||||
|
||||
expect(context.mockTokenBlacklistService.addToBlacklist).not.toHaveBeenCalled();
|
||||
expect(
|
||||
context.mockTokenBlacklistService.addToBlacklist,
|
||||
).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should validate token format before decoding', async () => {
|
||||
@@ -214,7 +236,9 @@ describe('AuthService - logout', () => {
|
||||
|
||||
await Promise.all(promises);
|
||||
|
||||
expect(context.mockTokenBlacklistService.addToBlacklist).toHaveBeenCalledTimes(3);
|
||||
expect(
|
||||
context.mockTokenBlacklistService.addToBlacklist,
|
||||
).toHaveBeenCalledTimes(3);
|
||||
});
|
||||
|
||||
it('should validate decoded payload structure', async () => {
|
||||
@@ -223,11 +247,15 @@ describe('AuthService - logout', () => {
|
||||
} as any);
|
||||
|
||||
await expect(
|
||||
context.service.logout('token.with.invalid.structure')
|
||||
context.service.logout('token.with.invalid.structure'),
|
||||
).rejects.toThrow('Token inválido ou não pode ser decodificado');
|
||||
|
||||
expect(context.mockSessionManagementService.terminateSession).not.toHaveBeenCalled();
|
||||
expect(context.mockTokenBlacklistService.addToBlacklist).not.toHaveBeenCalled();
|
||||
expect(
|
||||
context.mockSessionManagementService.terminateSession,
|
||||
).not.toHaveBeenCalled();
|
||||
expect(
|
||||
context.mockTokenBlacklistService.addToBlacklist,
|
||||
).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should ensure token is always blacklisted on success', async () => {
|
||||
@@ -235,8 +263,12 @@ describe('AuthService - logout', () => {
|
||||
|
||||
await context.service.logout('valid.token');
|
||||
|
||||
expect(context.mockTokenBlacklistService.addToBlacklist).toHaveBeenCalledWith('valid.token');
|
||||
expect(context.mockTokenBlacklistService.addToBlacklist).toHaveBeenCalledTimes(1);
|
||||
expect(
|
||||
context.mockTokenBlacklistService.addToBlacklist,
|
||||
).toHaveBeenCalledWith('valid.token');
|
||||
expect(
|
||||
context.mockTokenBlacklistService.addToBlacklist,
|
||||
).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
it('should handle race condition when token becomes blacklisted between check and add', async () => {
|
||||
@@ -248,13 +280,17 @@ describe('AuthService - logout', () => {
|
||||
*/
|
||||
context.mockTokenBlacklistService.isBlacklisted.mockResolvedValue(false);
|
||||
context.mockTokenBlacklistService.addToBlacklist.mockRejectedValue(
|
||||
new Error('Token já está na blacklist')
|
||||
new Error('Token já está na blacklist'),
|
||||
);
|
||||
|
||||
await context.service.logout('token.with.race.condition');
|
||||
|
||||
expect(context.mockTokenBlacklistService.isBlacklisted).toHaveBeenCalledWith('token.with.race.condition');
|
||||
expect(context.mockTokenBlacklistService.addToBlacklist).toHaveBeenCalledWith('token.with.race.condition');
|
||||
expect(
|
||||
context.mockTokenBlacklistService.isBlacklisted,
|
||||
).toHaveBeenCalledWith('token.with.race.condition');
|
||||
expect(
|
||||
context.mockTokenBlacklistService.addToBlacklist,
|
||||
).toHaveBeenCalledWith('token.with.race.condition');
|
||||
});
|
||||
|
||||
it('should throw error if addToBlacklist fails with non-blacklist error', async () => {
|
||||
@@ -265,15 +301,21 @@ describe('AuthService - logout', () => {
|
||||
*/
|
||||
context.mockTokenBlacklistService.isBlacklisted.mockResolvedValue(false);
|
||||
context.mockTokenBlacklistService.addToBlacklist.mockRejectedValue(
|
||||
new Error('Erro de conexão com Redis')
|
||||
new Error('Erro de conexão com Redis'),
|
||||
);
|
||||
|
||||
await expect(
|
||||
context.service.logout('token.with.blacklist.error')
|
||||
).rejects.toThrow('Falha ao adicionar token à blacklist: Erro de conexão com Redis');
|
||||
context.service.logout('token.with.blacklist.error'),
|
||||
).rejects.toThrow(
|
||||
'Falha ao adicionar token à blacklist: Erro de conexão com Redis',
|
||||
);
|
||||
|
||||
expect(context.mockTokenBlacklistService.isBlacklisted).toHaveBeenCalledWith('token.with.blacklist.error');
|
||||
expect(context.mockTokenBlacklistService.addToBlacklist).toHaveBeenCalledWith('token.with.blacklist.error');
|
||||
expect(
|
||||
context.mockTokenBlacklistService.isBlacklisted,
|
||||
).toHaveBeenCalledWith('token.with.blacklist.error');
|
||||
expect(
|
||||
context.mockTokenBlacklistService.addToBlacklist,
|
||||
).toHaveBeenCalledWith('token.with.blacklist.error');
|
||||
});
|
||||
|
||||
it('should verify isBlacklisted is called before addToBlacklist', async () => {
|
||||
@@ -286,11 +328,14 @@ describe('AuthService - logout', () => {
|
||||
|
||||
await context.service.logout('valid.token');
|
||||
|
||||
const isBlacklistedCallOrder = context.mockTokenBlacklistService.isBlacklisted.mock.invocationCallOrder[0];
|
||||
const addToBlacklistCallOrder = context.mockTokenBlacklistService.addToBlacklist.mock.invocationCallOrder[0];
|
||||
const isBlacklistedCallOrder =
|
||||
context.mockTokenBlacklistService.isBlacklisted.mock
|
||||
.invocationCallOrder[0];
|
||||
const addToBlacklistCallOrder =
|
||||
context.mockTokenBlacklistService.addToBlacklist.mock
|
||||
.invocationCallOrder[0];
|
||||
|
||||
expect(isBlacklistedCallOrder).toBeLessThan(addToBlacklistCallOrder);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
@@ -19,7 +19,9 @@ describe('AuthService - refreshAccessToken', () => {
|
||||
situacao: 'A',
|
||||
dataDesligamento: null,
|
||||
});
|
||||
context.mockSessionManagementService.isSessionActive.mockResolvedValue(true);
|
||||
context.mockSessionManagementService.isSessionActive.mockResolvedValue(
|
||||
true,
|
||||
);
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
@@ -29,7 +31,7 @@ describe('AuthService - refreshAccessToken', () => {
|
||||
describe('refreshAccessToken - Tests that expose problems', () => {
|
||||
/**
|
||||
* NOTA: Estes testes identificam problemas no método refreshAccessToken.
|
||||
*
|
||||
*
|
||||
* PROBLEMAS IDENTIFICADOS:
|
||||
* 1. Não valida refresh token antes de processar
|
||||
* 2. Não valida dados retornados pelo refresh token service
|
||||
@@ -40,35 +42,43 @@ describe('AuthService - refreshAccessToken', () => {
|
||||
*/
|
||||
|
||||
it('should reject empty refresh token', async () => {
|
||||
await expect(
|
||||
context.service.refreshAccessToken('')
|
||||
).rejects.toThrow('Refresh token não pode estar vazio');
|
||||
await expect(context.service.refreshAccessToken('')).rejects.toThrow(
|
||||
'Refresh token não pode estar vazio',
|
||||
);
|
||||
|
||||
expect(context.mockRefreshTokenService.validateRefreshToken).not.toHaveBeenCalled();
|
||||
expect(
|
||||
context.mockRefreshTokenService.validateRefreshToken,
|
||||
).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should reject null refresh token', async () => {
|
||||
await expect(
|
||||
context.service.refreshAccessToken(null as any)
|
||||
context.service.refreshAccessToken(null as any),
|
||||
).rejects.toThrow('Refresh token não pode estar vazio');
|
||||
|
||||
expect(context.mockRefreshTokenService.validateRefreshToken).not.toHaveBeenCalled();
|
||||
expect(
|
||||
context.mockRefreshTokenService.validateRefreshToken,
|
||||
).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should reject undefined refresh token', async () => {
|
||||
await expect(
|
||||
context.service.refreshAccessToken(undefined as any)
|
||||
context.service.refreshAccessToken(undefined as any),
|
||||
).rejects.toThrow('Refresh token não pode estar vazio');
|
||||
|
||||
expect(context.mockRefreshTokenService.validateRefreshToken).not.toHaveBeenCalled();
|
||||
expect(
|
||||
context.mockRefreshTokenService.validateRefreshToken,
|
||||
).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should reject whitespace-only refresh token', async () => {
|
||||
await expect(
|
||||
context.service.refreshAccessToken(' ')
|
||||
).rejects.toThrow('Refresh token não pode estar vazio');
|
||||
await expect(context.service.refreshAccessToken(' ')).rejects.toThrow(
|
||||
'Refresh token não pode estar vazio',
|
||||
);
|
||||
|
||||
expect(context.mockRefreshTokenService.validateRefreshToken).not.toHaveBeenCalled();
|
||||
expect(
|
||||
context.mockRefreshTokenService.validateRefreshToken,
|
||||
).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should validate tokenData has required id field', async () => {
|
||||
@@ -77,15 +87,17 @@ describe('AuthService - refreshAccessToken', () => {
|
||||
} as any);
|
||||
|
||||
await expect(
|
||||
context.service.refreshAccessToken('valid.refresh.token')
|
||||
context.service.refreshAccessToken('valid.refresh.token'),
|
||||
).rejects.toThrow('Dados do refresh token inválidos');
|
||||
});
|
||||
|
||||
it('should validate tokenData is not null', async () => {
|
||||
context.mockRefreshTokenService.validateRefreshToken.mockResolvedValue(null);
|
||||
context.mockRefreshTokenService.validateRefreshToken.mockResolvedValue(
|
||||
null,
|
||||
);
|
||||
|
||||
await expect(
|
||||
context.service.refreshAccessToken('valid.refresh.token')
|
||||
context.service.refreshAccessToken('valid.refresh.token'),
|
||||
).rejects.toThrow('Dados do refresh token inválidos');
|
||||
});
|
||||
|
||||
@@ -101,7 +113,7 @@ describe('AuthService - refreshAccessToken', () => {
|
||||
});
|
||||
|
||||
await expect(
|
||||
context.service.refreshAccessToken('valid.refresh.token')
|
||||
context.service.refreshAccessToken('valid.refresh.token'),
|
||||
).rejects.toThrow('Dados do usuário incompletos');
|
||||
});
|
||||
|
||||
@@ -117,7 +129,7 @@ describe('AuthService - refreshAccessToken', () => {
|
||||
});
|
||||
|
||||
await expect(
|
||||
context.service.refreshAccessToken('valid.refresh.token')
|
||||
context.service.refreshAccessToken('valid.refresh.token'),
|
||||
).rejects.toThrow('Dados do usuário incompletos');
|
||||
});
|
||||
|
||||
@@ -133,7 +145,7 @@ describe('AuthService - refreshAccessToken', () => {
|
||||
});
|
||||
|
||||
await expect(
|
||||
context.service.refreshAccessToken('valid.refresh.token')
|
||||
context.service.refreshAccessToken('valid.refresh.token'),
|
||||
).rejects.toThrow('Dados do usuário incompletos');
|
||||
});
|
||||
|
||||
@@ -141,7 +153,7 @@ describe('AuthService - refreshAccessToken', () => {
|
||||
context.mockJwtService.sign.mockReturnValue('');
|
||||
|
||||
await expect(
|
||||
context.service.refreshAccessToken('valid.refresh.token')
|
||||
context.service.refreshAccessToken('valid.refresh.token'),
|
||||
).rejects.toThrow('Falha ao gerar novo token de acesso');
|
||||
});
|
||||
|
||||
@@ -149,7 +161,7 @@ describe('AuthService - refreshAccessToken', () => {
|
||||
context.mockJwtService.sign.mockReturnValue(null as any);
|
||||
|
||||
await expect(
|
||||
context.service.refreshAccessToken('valid.refresh.token')
|
||||
context.service.refreshAccessToken('valid.refresh.token'),
|
||||
).rejects.toThrow('Falha ao gerar novo token de acesso');
|
||||
});
|
||||
|
||||
@@ -159,10 +171,12 @@ describe('AuthService - refreshAccessToken', () => {
|
||||
sessionId: 'expired-session',
|
||||
});
|
||||
|
||||
context.mockSessionManagementService.isSessionActive = jest.fn().mockResolvedValue(false);
|
||||
context.mockSessionManagementService.isSessionActive = jest
|
||||
.fn()
|
||||
.mockResolvedValue(false);
|
||||
|
||||
await expect(
|
||||
context.service.refreshAccessToken('valid.refresh.token')
|
||||
context.service.refreshAccessToken('valid.refresh.token'),
|
||||
).rejects.toThrow('Sessão não está mais ativa');
|
||||
});
|
||||
|
||||
@@ -178,7 +192,7 @@ describe('AuthService - refreshAccessToken', () => {
|
||||
});
|
||||
|
||||
await expect(
|
||||
context.service.refreshAccessToken('valid.refresh.token')
|
||||
context.service.refreshAccessToken('valid.refresh.token'),
|
||||
).rejects.toThrow('ID de vendedor inválido');
|
||||
});
|
||||
|
||||
@@ -186,24 +200,30 @@ describe('AuthService - refreshAccessToken', () => {
|
||||
const hugeToken = 'a'.repeat(100000);
|
||||
|
||||
await expect(
|
||||
context.service.refreshAccessToken(hugeToken)
|
||||
context.service.refreshAccessToken(hugeToken),
|
||||
).rejects.toThrow('Refresh token muito longo');
|
||||
|
||||
expect(context.mockRefreshTokenService.validateRefreshToken).not.toHaveBeenCalled();
|
||||
expect(
|
||||
context.mockRefreshTokenService.validateRefreshToken,
|
||||
).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should sanitize refresh token input', async () => {
|
||||
const maliciousToken = "'; DROP TABLE users; --";
|
||||
|
||||
await expect(
|
||||
context.service.refreshAccessToken(maliciousToken)
|
||||
context.service.refreshAccessToken(maliciousToken),
|
||||
).rejects.toThrow('Formato de refresh token inválido');
|
||||
|
||||
expect(context.mockRefreshTokenService.validateRefreshToken).not.toHaveBeenCalled();
|
||||
expect(
|
||||
context.mockRefreshTokenService.validateRefreshToken,
|
||||
).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should include only required fields in response', async () => {
|
||||
const result = await context.service.refreshAccessToken('valid.refresh.token');
|
||||
const result = await context.service.refreshAccessToken(
|
||||
'valid.refresh.token',
|
||||
);
|
||||
|
||||
expect(result).toHaveProperty('accessToken');
|
||||
expect(result).toHaveProperty('expiresIn');
|
||||
@@ -213,7 +233,9 @@ describe('AuthService - refreshAccessToken', () => {
|
||||
});
|
||||
|
||||
it('should validate expiresIn is correct', async () => {
|
||||
const result = await context.service.refreshAccessToken('valid.refresh.token');
|
||||
const result = await context.service.refreshAccessToken(
|
||||
'valid.refresh.token',
|
||||
);
|
||||
|
||||
expect(result.expiresIn).toBe(28800);
|
||||
expect(result.expiresIn).toBeGreaterThan(0);
|
||||
@@ -231,7 +253,7 @@ describe('AuthService - refreshAccessToken', () => {
|
||||
});
|
||||
|
||||
await expect(
|
||||
context.service.refreshAccessToken('valid.refresh.token')
|
||||
context.service.refreshAccessToken('valid.refresh.token'),
|
||||
).rejects.toThrow();
|
||||
});
|
||||
|
||||
@@ -244,7 +266,7 @@ describe('AuthService - refreshAccessToken', () => {
|
||||
|
||||
const results = await Promise.all(promises);
|
||||
|
||||
results.forEach(result => {
|
||||
results.forEach((result) => {
|
||||
expect(result).toHaveProperty('accessToken');
|
||||
expect(result.accessToken).toBeTruthy();
|
||||
});
|
||||
@@ -262,9 +284,8 @@ describe('AuthService - refreshAccessToken', () => {
|
||||
});
|
||||
|
||||
await expect(
|
||||
context.service.refreshAccessToken('valid.refresh.token')
|
||||
context.service.refreshAccessToken('valid.refresh.token'),
|
||||
).rejects.toThrow('Usuário inválido ou inativo');
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
@@ -24,14 +24,17 @@ import { RateLimitingGuard } from '../guards/rate-limiting.guard';
|
||||
import { RateLimitingService } from '../services/rate-limiting.service';
|
||||
import { RefreshTokenService } from '../services/refresh-token.service';
|
||||
import { SessionManagementService } from '../services/session-management.service';
|
||||
import { RefreshTokenDto, RefreshTokenResponseDto } from './dto/refresh-token.dto';
|
||||
import {
|
||||
RefreshTokenDto,
|
||||
RefreshTokenResponseDto,
|
||||
} from './dto/refresh-token.dto';
|
||||
import { SessionsResponseDto } from './dto/session.dto';
|
||||
import { LoginAuditService } from '../services/login-audit.service';
|
||||
import {
|
||||
LoginAuditFiltersDto,
|
||||
LoginAuditResponseDto,
|
||||
LoginStatsDto,
|
||||
LoginStatsFiltersDto
|
||||
import {
|
||||
LoginAuditFiltersDto,
|
||||
LoginAuditResponseDto,
|
||||
LoginStatsDto,
|
||||
LoginStatsFiltersDto,
|
||||
} from './dto/login-audit.dto';
|
||||
import {
|
||||
ApiTags,
|
||||
@@ -66,9 +69,12 @@ export class AuthController {
|
||||
})
|
||||
@ApiUnauthorizedResponse({ description: 'Usuário ou senha inválidos' })
|
||||
@ApiTooManyRequestsResponse({ description: 'Muitas tentativas de login' })
|
||||
async login(@Body() dto: LoginDto, @Request() req): Promise<LoginResponseDto> {
|
||||
async login(
|
||||
@Body() dto: LoginDto,
|
||||
@Request() req,
|
||||
): Promise<LoginResponseDto> {
|
||||
const ip = this.getClientIp(req);
|
||||
|
||||
|
||||
const command = new AuthenticateUserCommand(dto.username, dto.password);
|
||||
const result = await this.commandBus.execute(command);
|
||||
|
||||
@@ -76,7 +82,7 @@ export class AuthController {
|
||||
|
||||
if (!result.success) {
|
||||
await this.rateLimitingService.recordAttempt(ip, false);
|
||||
|
||||
|
||||
await this.loginAuditService.logLoginAttempt({
|
||||
username: dto.username,
|
||||
ipAddress: ip,
|
||||
@@ -84,7 +90,7 @@ export class AuthController {
|
||||
success: false,
|
||||
failureReason: result.error,
|
||||
});
|
||||
|
||||
|
||||
throw new HttpException(
|
||||
new ResultModel(false, result.error, null, result.error),
|
||||
HttpStatus.UNAUTHORIZED,
|
||||
@@ -94,19 +100,23 @@ export class AuthController {
|
||||
await this.rateLimitingService.recordAttempt(ip, true);
|
||||
|
||||
const user = result.data;
|
||||
|
||||
|
||||
/**
|
||||
* Verifica se o usuário já possui uma sessão ativa
|
||||
*/
|
||||
const existingSession = await this.sessionManagementService.hasActiveSession(user.id);
|
||||
|
||||
const existingSession =
|
||||
await this.sessionManagementService.hasActiveSession(user.id);
|
||||
|
||||
if (existingSession) {
|
||||
/**
|
||||
* Encerra a sessão existente antes de criar uma nova
|
||||
*/
|
||||
await this.sessionManagementService.terminateSession(user.id, existingSession.sessionId);
|
||||
await this.sessionManagementService.terminateSession(
|
||||
user.id,
|
||||
existingSession.sessionId,
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
const session = await this.sessionManagementService.createSession(
|
||||
user.id,
|
||||
ip,
|
||||
@@ -161,7 +171,6 @@ export class AuthController {
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@Post('logout')
|
||||
@UseGuards(JwtAuthGuard)
|
||||
@ApiBearerAuth()
|
||||
@@ -170,10 +179,15 @@ export class AuthController {
|
||||
@ApiUnauthorizedResponse({ description: 'Token inválido ou expirado' })
|
||||
async logout(@Request() req): Promise<{ message: string }> {
|
||||
const token = req.headers.authorization?.replace('Bearer ', '');
|
||||
|
||||
|
||||
if (!token) {
|
||||
throw new HttpException(
|
||||
new ResultModel(false, 'Token não fornecido', null, 'Token não fornecido'),
|
||||
new ResultModel(
|
||||
false,
|
||||
'Token não fornecido',
|
||||
null,
|
||||
'Token não fornecido',
|
||||
),
|
||||
HttpStatus.UNAUTHORIZED,
|
||||
);
|
||||
}
|
||||
@@ -192,8 +206,12 @@ export class AuthController {
|
||||
description: 'Token renovado com sucesso',
|
||||
type: RefreshTokenResponseDto,
|
||||
})
|
||||
@ApiUnauthorizedResponse({ description: 'Refresh token inválido ou expirado' })
|
||||
async refreshToken(@Body() dto: RefreshTokenDto): Promise<RefreshTokenResponseDto> {
|
||||
@ApiUnauthorizedResponse({
|
||||
description: 'Refresh token inválido ou expirado',
|
||||
})
|
||||
async refreshToken(
|
||||
@Body() dto: RefreshTokenDto,
|
||||
): Promise<RefreshTokenResponseDto> {
|
||||
const result = await this.authService.refreshAccessToken(dto.refreshToken);
|
||||
return result;
|
||||
}
|
||||
@@ -210,15 +228,20 @@ export class AuthController {
|
||||
async getSessions(@Request() req): Promise<SessionsResponseDto> {
|
||||
const userId = req.user.id;
|
||||
const currentSessionId = req.user.sessionId;
|
||||
const sessions = await this.sessionManagementService.getActiveSessions(userId, currentSessionId);
|
||||
|
||||
const sessions = await this.sessionManagementService.getActiveSessions(
|
||||
userId,
|
||||
currentSessionId,
|
||||
);
|
||||
|
||||
return {
|
||||
sessions: sessions.map(session => ({
|
||||
sessions: sessions.map((session) => ({
|
||||
sessionId: session.sessionId,
|
||||
ipAddress: session.ipAddress,
|
||||
userAgent: session.userAgent,
|
||||
createdAt: DateUtil.toBrazilISOString(new Date(session.createdAt)),
|
||||
lastActivity: DateUtil.toBrazilISOString(new Date(session.lastActivity)),
|
||||
lastActivity: DateUtil.toBrazilISOString(
|
||||
new Date(session.lastActivity),
|
||||
),
|
||||
isCurrent: session.sessionId === currentSessionId,
|
||||
})),
|
||||
total: sessions.length,
|
||||
@@ -238,7 +261,7 @@ export class AuthController {
|
||||
): Promise<{ message: string }> {
|
||||
const userId = req.user.id;
|
||||
await this.sessionManagementService.terminateSession(userId, sessionId);
|
||||
|
||||
|
||||
return {
|
||||
message: 'Sessão encerrada com sucesso',
|
||||
};
|
||||
@@ -253,7 +276,7 @@ export class AuthController {
|
||||
async terminateAllSessions(@Request() req): Promise<{ message: string }> {
|
||||
const userId = req.user.id;
|
||||
await this.sessionManagementService.terminateAllSessions(userId);
|
||||
|
||||
|
||||
return {
|
||||
message: 'Todas as sessões foram encerradas com sucesso',
|
||||
};
|
||||
@@ -273,7 +296,7 @@ export class AuthController {
|
||||
@Request() req,
|
||||
): Promise<LoginAuditResponseDto> {
|
||||
const userId = req.user.id;
|
||||
|
||||
|
||||
const auditFilters = {
|
||||
...filters,
|
||||
userId: filters.userId || userId,
|
||||
@@ -282,9 +305,9 @@ export class AuthController {
|
||||
};
|
||||
|
||||
const logs = await this.loginAuditService.getLoginLogs(auditFilters);
|
||||
|
||||
|
||||
return {
|
||||
logs: logs.map(log => ({
|
||||
logs: logs.map((log) => ({
|
||||
...log,
|
||||
timestamp: DateUtil.toBrazilISOString(log.timestamp),
|
||||
})),
|
||||
@@ -309,12 +332,12 @@ export class AuthController {
|
||||
): Promise<LoginStatsDto> {
|
||||
const userId = req.user.id;
|
||||
const days = filters.days || 7;
|
||||
|
||||
|
||||
const stats = await this.loginAuditService.getLoginStats(
|
||||
filters.userId || userId,
|
||||
days,
|
||||
);
|
||||
|
||||
|
||||
return stats;
|
||||
}
|
||||
|
||||
@@ -333,13 +356,12 @@ export class AuthController {
|
||||
ipAddress: { type: 'string' },
|
||||
userAgent: { type: 'string' },
|
||||
createdAt: { type: 'string' },
|
||||
lastActivity: { type: 'string' }
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
lastActivity: { type: 'string' },
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
})
|
||||
|
||||
@Get('session/status')
|
||||
async checkSessionStatus(@Query('username') username: string): Promise<{
|
||||
hasActiveSession: boolean;
|
||||
@@ -353,7 +375,12 @@ export class AuthController {
|
||||
}> {
|
||||
if (!username) {
|
||||
throw new HttpException(
|
||||
new ResultModel(false, 'Username é obrigatório', null, 'Username é obrigatório'),
|
||||
new ResultModel(
|
||||
false,
|
||||
'Username é obrigatório',
|
||||
null,
|
||||
'Username é obrigatório',
|
||||
),
|
||||
HttpStatus.BAD_REQUEST,
|
||||
);
|
||||
}
|
||||
@@ -362,15 +389,17 @@ export class AuthController {
|
||||
* Busca o usuário pelo username para obter o ID
|
||||
*/
|
||||
const user = await this.authService.findUserByUsername(username);
|
||||
|
||||
|
||||
if (!user) {
|
||||
return {
|
||||
hasActiveSession: false,
|
||||
};
|
||||
}
|
||||
|
||||
const activeSession = await this.sessionManagementService.hasActiveSession(user.id);
|
||||
|
||||
const activeSession = await this.sessionManagementService.hasActiveSession(
|
||||
user.id,
|
||||
);
|
||||
|
||||
if (!activeSession) {
|
||||
return {
|
||||
hasActiveSession: false,
|
||||
@@ -383,8 +412,12 @@ export class AuthController {
|
||||
sessionId: activeSession.sessionId,
|
||||
ipAddress: activeSession.ipAddress,
|
||||
userAgent: activeSession.userAgent,
|
||||
createdAt: DateUtil.toBrazilISOString(new Date(activeSession.createdAt)),
|
||||
lastActivity: DateUtil.toBrazilISOString(new Date(activeSession.lastActivity)),
|
||||
createdAt: DateUtil.toBrazilISOString(
|
||||
new Date(activeSession.createdAt),
|
||||
),
|
||||
lastActivity: DateUtil.toBrazilISOString(
|
||||
new Date(activeSession.lastActivity),
|
||||
),
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
@@ -35,14 +35,14 @@ import { LoginAuditService } from '../services/login-audit.service';
|
||||
],
|
||||
controllers: [AuthController],
|
||||
providers: [
|
||||
AuthService,
|
||||
JwtStrategy,
|
||||
TokenBlacklistService,
|
||||
RateLimitingService,
|
||||
AuthService,
|
||||
JwtStrategy,
|
||||
TokenBlacklistService,
|
||||
RateLimitingService,
|
||||
RefreshTokenService,
|
||||
SessionManagementService,
|
||||
LoginAuditService,
|
||||
AuthenticateUserHandler
|
||||
AuthenticateUserHandler,
|
||||
],
|
||||
exports: [AuthService],
|
||||
})
|
||||
|
||||
@@ -1,4 +1,8 @@
|
||||
import { Injectable, UnauthorizedException, BadRequestException } from '@nestjs/common';
|
||||
import {
|
||||
Injectable,
|
||||
UnauthorizedException,
|
||||
BadRequestException,
|
||||
} from '@nestjs/common';
|
||||
import { JwtService, JwtSignOptions } from '@nestjs/jwt';
|
||||
import { UsersService } from '../users/users.service';
|
||||
import { JwtPayload } from '../models/jwt-payload.model';
|
||||
@@ -7,7 +11,6 @@ import { TokenBlacklistService } from '../services/token-blacklist.service';
|
||||
import { RefreshTokenService } from '../services/refresh-token.service';
|
||||
import { SessionManagementService } from '../services/session-management.service';
|
||||
|
||||
|
||||
@Injectable()
|
||||
export class AuthService {
|
||||
constructor(
|
||||
@@ -23,7 +26,14 @@ export class AuthService {
|
||||
* Cria um token JWT com validação de todos os parâmetros de entrada
|
||||
* @throws BadRequestException quando os parâmetros são inválidos
|
||||
*/
|
||||
async createToken(id: number, sellerId: number, username: string, email: string, storeId: string, sessionId?: string) {
|
||||
async createToken(
|
||||
id: number,
|
||||
sellerId: number,
|
||||
username: string,
|
||||
email: string,
|
||||
storeId: string,
|
||||
sessionId?: string,
|
||||
) {
|
||||
this.validateTokenParameters(id, sellerId, username, email, storeId);
|
||||
|
||||
const user: JwtPayload = {
|
||||
@@ -42,7 +52,13 @@ export class AuthService {
|
||||
* Valida os parâmetros de entrada para criação de token
|
||||
* @private
|
||||
*/
|
||||
private validateTokenParameters(id: number, sellerId: number, username: string, email: string, storeId: string): void {
|
||||
private validateTokenParameters(
|
||||
id: number,
|
||||
sellerId: number,
|
||||
username: string,
|
||||
email: string,
|
||||
storeId: string,
|
||||
): void {
|
||||
if (!id || id <= 0) {
|
||||
throw new BadRequestException('ID de usuário inválido');
|
||||
}
|
||||
@@ -64,7 +80,9 @@ export class AuthService {
|
||||
}
|
||||
|
||||
if (/['";\\]/.test(username)) {
|
||||
throw new BadRequestException('Nome de usuário contém caracteres inválidos');
|
||||
throw new BadRequestException(
|
||||
'Nome de usuário contém caracteres inválidos',
|
||||
);
|
||||
}
|
||||
|
||||
if (!email || typeof email !== 'string' || !email.trim()) {
|
||||
@@ -77,7 +95,7 @@ export class AuthService {
|
||||
|
||||
const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
|
||||
const multipleAtSymbols = (email.match(/@/g) || []).length > 1;
|
||||
|
||||
|
||||
if (!emailRegex.test(email) || multipleAtSymbols) {
|
||||
throw new BadRequestException('Formato de email inválido');
|
||||
}
|
||||
@@ -92,16 +110,41 @@ export class AuthService {
|
||||
* @throws BadRequestException quando os parâmetros são inválidos
|
||||
* @throws Error quando os tokens gerados são inválidos
|
||||
*/
|
||||
async createTokenPair(id: number, sellerId: number, username: string, email: string, storeId: string, sessionId?: string) {
|
||||
const accessToken = await this.createToken(id, sellerId, username, email, storeId, sessionId);
|
||||
|
||||
if (!accessToken || typeof accessToken !== 'string' || !accessToken.trim()) {
|
||||
async createTokenPair(
|
||||
id: number,
|
||||
sellerId: number,
|
||||
username: string,
|
||||
email: string,
|
||||
storeId: string,
|
||||
sessionId?: string,
|
||||
) {
|
||||
const accessToken = await this.createToken(
|
||||
id,
|
||||
sellerId,
|
||||
username,
|
||||
email,
|
||||
storeId,
|
||||
sessionId,
|
||||
);
|
||||
|
||||
if (
|
||||
!accessToken ||
|
||||
typeof accessToken !== 'string' ||
|
||||
!accessToken.trim()
|
||||
) {
|
||||
throw new Error('Token de acesso inválido gerado');
|
||||
}
|
||||
|
||||
const refreshToken = await this.refreshTokenService.generateRefreshToken(id, sessionId);
|
||||
|
||||
if (!refreshToken || typeof refreshToken !== 'string' || !refreshToken.trim()) {
|
||||
const refreshToken = await this.refreshTokenService.generateRefreshToken(
|
||||
id,
|
||||
sessionId,
|
||||
);
|
||||
|
||||
if (
|
||||
!refreshToken ||
|
||||
typeof refreshToken !== 'string' ||
|
||||
!refreshToken.trim()
|
||||
) {
|
||||
throw new Error('Refresh token inválido gerado');
|
||||
}
|
||||
|
||||
@@ -121,8 +164,10 @@ export class AuthService {
|
||||
async refreshAccessToken(refreshToken: string) {
|
||||
this.validateRefreshTokenInput(refreshToken);
|
||||
|
||||
const tokenData = await this.refreshTokenService.validateRefreshToken(refreshToken);
|
||||
|
||||
const tokenData = await this.refreshTokenService.validateRefreshToken(
|
||||
refreshToken,
|
||||
);
|
||||
|
||||
if (!tokenData || !tokenData.id) {
|
||||
throw new BadRequestException('Dados do refresh token inválidos');
|
||||
}
|
||||
@@ -135,10 +180,11 @@ export class AuthService {
|
||||
this.validateUserDataForToken(user);
|
||||
|
||||
if (tokenData.sessionId) {
|
||||
const isSessionActive = await this.sessionManagementService.isSessionActive(
|
||||
user.id,
|
||||
tokenData.sessionId
|
||||
);
|
||||
const isSessionActive =
|
||||
await this.sessionManagementService.isSessionActive(
|
||||
user.id,
|
||||
tokenData.sessionId,
|
||||
);
|
||||
if (!isSessionActive) {
|
||||
throw new UnauthorizedException('Sessão não está mais ativa');
|
||||
}
|
||||
@@ -150,10 +196,14 @@ export class AuthService {
|
||||
user.name,
|
||||
user.email,
|
||||
user.storeId,
|
||||
tokenData.sessionId
|
||||
tokenData.sessionId,
|
||||
);
|
||||
|
||||
if (!newAccessToken || typeof newAccessToken !== 'string' || !newAccessToken.trim()) {
|
||||
if (
|
||||
!newAccessToken ||
|
||||
typeof newAccessToken !== 'string' ||
|
||||
!newAccessToken.trim()
|
||||
) {
|
||||
throw new Error('Falha ao gerar novo token de acesso');
|
||||
}
|
||||
|
||||
@@ -168,7 +218,11 @@ export class AuthService {
|
||||
* @private
|
||||
*/
|
||||
private validateRefreshTokenInput(refreshToken: string): void {
|
||||
if (!refreshToken || typeof refreshToken !== 'string' || !refreshToken.trim()) {
|
||||
if (
|
||||
!refreshToken ||
|
||||
typeof refreshToken !== 'string' ||
|
||||
!refreshToken.trim()
|
||||
) {
|
||||
throw new BadRequestException('Refresh token não pode estar vazio');
|
||||
}
|
||||
|
||||
@@ -187,18 +241,32 @@ export class AuthService {
|
||||
*/
|
||||
private validateUserDataForToken(user: any): void {
|
||||
if (!user.name || typeof user.name !== 'string' || !user.name.trim()) {
|
||||
throw new BadRequestException('Dados do usuário incompletos: nome não encontrado');
|
||||
throw new BadRequestException(
|
||||
'Dados do usuário incompletos: nome não encontrado',
|
||||
);
|
||||
}
|
||||
|
||||
if (!user.email || typeof user.email !== 'string' || !user.email.trim()) {
|
||||
throw new BadRequestException('Dados do usuário incompletos: email não encontrado');
|
||||
throw new BadRequestException(
|
||||
'Dados do usuário incompletos: email não encontrado',
|
||||
);
|
||||
}
|
||||
|
||||
if (!user.storeId || typeof user.storeId !== 'string' || !user.storeId.trim()) {
|
||||
throw new BadRequestException('Dados do usuário incompletos: storeId não encontrado');
|
||||
if (
|
||||
!user.storeId ||
|
||||
typeof user.storeId !== 'string' ||
|
||||
!user.storeId.trim()
|
||||
) {
|
||||
throw new BadRequestException(
|
||||
'Dados do usuário incompletos: storeId não encontrado',
|
||||
);
|
||||
}
|
||||
|
||||
if (user.sellerId !== null && user.sellerId !== undefined && user.sellerId < 0) {
|
||||
if (
|
||||
user.sellerId !== null &&
|
||||
user.sellerId !== undefined &&
|
||||
user.sellerId < 0
|
||||
) {
|
||||
throw new BadRequestException('ID de vendedor inválido');
|
||||
}
|
||||
}
|
||||
@@ -228,11 +296,15 @@ export class AuthService {
|
||||
try {
|
||||
decoded = this.jwtService.decode(token) as JwtPayload;
|
||||
} catch (error) {
|
||||
throw new BadRequestException('Token inválido ou não pode ser decodificado');
|
||||
throw new BadRequestException(
|
||||
'Token inválido ou não pode ser decodificado',
|
||||
);
|
||||
}
|
||||
|
||||
if (!decoded || !decoded.id) {
|
||||
throw new BadRequestException('Token inválido ou não pode ser decodificado');
|
||||
throw new BadRequestException(
|
||||
'Token inválido ou não pode ser decodificado',
|
||||
);
|
||||
}
|
||||
|
||||
if (decoded.id <= 0) {
|
||||
@@ -241,25 +313,34 @@ export class AuthService {
|
||||
|
||||
if (decoded.sessionId && decoded.id && decoded.sessionId.trim()) {
|
||||
try {
|
||||
await this.sessionManagementService.terminateSession(decoded.id, decoded.sessionId);
|
||||
await this.sessionManagementService.terminateSession(
|
||||
decoded.id,
|
||||
decoded.sessionId,
|
||||
);
|
||||
} catch (error) {
|
||||
const errorMessage = error instanceof Error ? error.message : String(error);
|
||||
const errorMessage =
|
||||
error instanceof Error ? error.message : String(error);
|
||||
if (errorMessage.includes('Sessão não encontrada')) {
|
||||
throw new Error('Sessão não encontrada');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const isAlreadyBlacklisted = await this.tokenBlacklistService.isBlacklisted(token);
|
||||
const isAlreadyBlacklisted = await this.tokenBlacklistService.isBlacklisted(
|
||||
token,
|
||||
);
|
||||
if (!isAlreadyBlacklisted) {
|
||||
try {
|
||||
await this.tokenBlacklistService.addToBlacklist(token);
|
||||
} catch (error) {
|
||||
const errorMessage = error instanceof Error ? error.message : String(error);
|
||||
const errorMessage =
|
||||
error instanceof Error ? error.message : String(error);
|
||||
if (errorMessage.includes('já está na blacklist')) {
|
||||
return;
|
||||
}
|
||||
throw new Error(`Falha ao adicionar token à blacklist: ${errorMessage}`);
|
||||
throw new Error(
|
||||
`Falha ao adicionar token à blacklist: ${errorMessage}`,
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -289,4 +370,4 @@ export class AuthService {
|
||||
async findUserByUsername(username: string) {
|
||||
return this.userRepository.findByUsername(username);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
export class AuthenticateUserCommand {
|
||||
constructor(
|
||||
public readonly username: string,
|
||||
public readonly password: string,
|
||||
) {}
|
||||
}
|
||||
|
||||
constructor(
|
||||
public readonly username: string,
|
||||
public readonly password: string,
|
||||
) {}
|
||||
}
|
||||
|
||||
@@ -7,13 +7,18 @@ import { UserModel } from 'src/core/models/user.model';
|
||||
|
||||
@CommandHandler(AuthenticateUserCommand)
|
||||
@Injectable()
|
||||
export class AuthenticateUserHandler implements ICommandHandler<AuthenticateUserCommand> {
|
||||
export class AuthenticateUserHandler
|
||||
implements ICommandHandler<AuthenticateUserCommand>
|
||||
{
|
||||
constructor(private readonly userRepository: UserRepository) {}
|
||||
|
||||
async execute(command: AuthenticateUserCommand): Promise<Result<UserModel>> {
|
||||
const { username, password } = command;
|
||||
|
||||
const user = await this.userRepository.findByUsernameAndPassword(username, password);
|
||||
const user = await this.userRepository.findByUsernameAndPassword(
|
||||
username,
|
||||
password,
|
||||
);
|
||||
|
||||
if (!user) {
|
||||
return Result.fail('Usuário ou senha inválidos');
|
||||
@@ -30,7 +35,6 @@ export class AuthenticateUserHandler implements ICommandHandler<AuthenticateUser
|
||||
if (user.situacao === 'B') {
|
||||
return Result.fail('Usuário bloqueado, login não permitido!');
|
||||
}
|
||||
|
||||
|
||||
return Result.ok(user);
|
||||
}
|
||||
|
||||
@@ -1,5 +1,13 @@
|
||||
import { ApiProperty } from '@nestjs/swagger';
|
||||
import { IsOptional, IsNumber, IsString, IsBoolean, IsDateString, Min, Max } from 'class-validator';
|
||||
import {
|
||||
IsOptional,
|
||||
IsNumber,
|
||||
IsString,
|
||||
IsBoolean,
|
||||
IsDateString,
|
||||
Min,
|
||||
Max,
|
||||
} from 'class-validator';
|
||||
import { Type } from 'class-transformer';
|
||||
|
||||
export class LoginAuditFiltersDto {
|
||||
@@ -19,7 +27,10 @@ export class LoginAuditFiltersDto {
|
||||
@IsString()
|
||||
ipAddress?: string;
|
||||
|
||||
@ApiProperty({ description: 'Filtrar apenas logins bem-sucedidos', required: false })
|
||||
@ApiProperty({
|
||||
description: 'Filtrar apenas logins bem-sucedidos',
|
||||
required: false,
|
||||
})
|
||||
@IsOptional()
|
||||
@IsBoolean()
|
||||
@Type(() => Boolean)
|
||||
@@ -35,7 +46,12 @@ export class LoginAuditFiltersDto {
|
||||
@IsDateString()
|
||||
endDate?: string;
|
||||
|
||||
@ApiProperty({ description: 'Número de registros por página', required: false, minimum: 1, maximum: 1000 })
|
||||
@ApiProperty({
|
||||
description: 'Número de registros por página',
|
||||
required: false,
|
||||
minimum: 1,
|
||||
maximum: 1000,
|
||||
})
|
||||
@IsOptional()
|
||||
@IsNumber()
|
||||
@Type(() => Number)
|
||||
@@ -43,7 +59,11 @@ export class LoginAuditFiltersDto {
|
||||
@Max(1000)
|
||||
limit?: number;
|
||||
|
||||
@ApiProperty({ description: 'Offset para paginação', required: false, minimum: 0 })
|
||||
@ApiProperty({
|
||||
description: 'Offset para paginação',
|
||||
required: false,
|
||||
minimum: 0,
|
||||
})
|
||||
@IsOptional()
|
||||
@IsNumber()
|
||||
@Type(() => Number)
|
||||
@@ -84,7 +104,10 @@ export class LoginAuditLogDto {
|
||||
}
|
||||
|
||||
export class LoginAuditResponseDto {
|
||||
@ApiProperty({ description: 'Lista de logs de login', type: [LoginAuditLogDto] })
|
||||
@ApiProperty({
|
||||
description: 'Lista de logs de login',
|
||||
type: [LoginAuditLogDto],
|
||||
})
|
||||
logs: LoginAuditLogDto[];
|
||||
|
||||
@ApiProperty({ description: 'Total de registros encontrados' })
|
||||
@@ -114,22 +137,30 @@ export class LoginStatsDto {
|
||||
topIps: Array<{ ip: string; count: number }>;
|
||||
|
||||
@ApiProperty({ description: 'Estatísticas diárias' })
|
||||
dailyStats: Array<{
|
||||
date: string;
|
||||
attempts: number;
|
||||
successes: number;
|
||||
failures: number;
|
||||
dailyStats: Array<{
|
||||
date: string;
|
||||
attempts: number;
|
||||
successes: number;
|
||||
failures: number;
|
||||
}>;
|
||||
}
|
||||
|
||||
export class LoginStatsFiltersDto {
|
||||
@ApiProperty({ description: 'ID do usuário para estatísticas', required: false })
|
||||
@ApiProperty({
|
||||
description: 'ID do usuário para estatísticas',
|
||||
required: false,
|
||||
})
|
||||
@IsOptional()
|
||||
@IsNumber()
|
||||
@Type(() => Number)
|
||||
userId?: number;
|
||||
|
||||
@ApiProperty({ description: 'Número de dias para análise', required: false, minimum: 1, maximum: 365 })
|
||||
@ApiProperty({
|
||||
description: 'Número de dias para análise',
|
||||
required: false,
|
||||
minimum: 1,
|
||||
maximum: 365,
|
||||
})
|
||||
@IsOptional()
|
||||
@IsNumber()
|
||||
@Type(() => Number)
|
||||
|
||||
@@ -50,7 +50,7 @@ describe('RateLimitingGuard - Tests that expose problems', () => {
|
||||
describe('canActivate', () => {
|
||||
/**
|
||||
* NOTA: Estes testes identificam problemas no método canActivate.
|
||||
*
|
||||
*
|
||||
* PROBLEMAS IDENTIFICADOS:
|
||||
* 1. Não valida se IP extraído é válido
|
||||
* 2. Não valida se rate limiting service retorna dados válidos
|
||||
@@ -196,7 +196,7 @@ describe('RateLimitingGuard - Tests that expose problems', () => {
|
||||
|
||||
mockGetRequest.mockReturnValue(request);
|
||||
mockRateLimitingService.isAllowed.mockRejectedValue(
|
||||
new Error('Erro de conexão com Redis')
|
||||
new Error('Erro de conexão com Redis'),
|
||||
);
|
||||
|
||||
try {
|
||||
@@ -225,7 +225,7 @@ describe('RateLimitingGuard - Tests that expose problems', () => {
|
||||
mockGetRequest.mockReturnValue(request);
|
||||
mockRateLimitingService.isAllowed.mockResolvedValue(false);
|
||||
mockRateLimitingService.getAttemptInfo.mockRejectedValue(
|
||||
new Error('Erro ao buscar informações')
|
||||
new Error('Erro ao buscar informações'),
|
||||
);
|
||||
|
||||
try {
|
||||
@@ -336,7 +336,9 @@ describe('RateLimitingGuard - Tests that expose problems', () => {
|
||||
const result = await guard.canActivate(mockExecutionContext);
|
||||
|
||||
expect(result).toBe(true);
|
||||
expect(mockRateLimitingService.isAllowed).toHaveBeenCalledWith('192.168.1.1');
|
||||
expect(mockRateLimitingService.isAllowed).toHaveBeenCalledWith(
|
||||
'192.168.1.1',
|
||||
);
|
||||
});
|
||||
|
||||
it('should handle concurrent requests with same IP', async () => {
|
||||
@@ -363,7 +365,7 @@ describe('RateLimitingGuard - Tests that expose problems', () => {
|
||||
|
||||
const results = await Promise.all(promises);
|
||||
|
||||
results.forEach(result => {
|
||||
results.forEach((result) => {
|
||||
expect(result).toBe(true);
|
||||
});
|
||||
});
|
||||
@@ -394,7 +396,9 @@ describe('RateLimitingGuard - Tests that expose problems', () => {
|
||||
fail('Deveria ter lançado exceção');
|
||||
} catch (error) {
|
||||
expect(error).toBeInstanceOf(HttpException);
|
||||
expect((error as HttpException).getStatus()).toBe(HttpStatus.TOO_MANY_REQUESTS);
|
||||
expect((error as HttpException).getStatus()).toBe(
|
||||
HttpStatus.TOO_MANY_REQUESTS,
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
@@ -419,7 +423,9 @@ describe('RateLimitingGuard - Tests that expose problems', () => {
|
||||
fail('Deveria ter lançado exceção');
|
||||
} catch (error) {
|
||||
const response = (error as HttpException).getResponse() as any;
|
||||
expect(response.error).toBe('Muitas tentativas de login. Tente novamente em alguns minutos.');
|
||||
expect(response.error).toBe(
|
||||
'Muitas tentativas de login. Tente novamente em alguns minutos.',
|
||||
);
|
||||
expect(response.success).toBe(false);
|
||||
}
|
||||
});
|
||||
@@ -512,7 +518,9 @@ describe('RateLimitingGuard - Tests that expose problems', () => {
|
||||
const result = await guard.canActivate(mockExecutionContext);
|
||||
|
||||
expect(result).toBe(true);
|
||||
expect(mockRateLimitingService.isAllowed).toHaveBeenCalledWith('2001:0db8:85a3:0000:0000:8a2e:0370:7334');
|
||||
expect(mockRateLimitingService.isAllowed).toHaveBeenCalledWith(
|
||||
'2001:0db8:85a3:0000:0000:8a2e:0370:7334',
|
||||
);
|
||||
});
|
||||
|
||||
it('should reject invalid IPv6 format', async () => {
|
||||
@@ -556,7 +564,9 @@ describe('RateLimitingGuard - Tests that expose problems', () => {
|
||||
|
||||
await guard.canActivate(mockExecutionContext);
|
||||
|
||||
expect(mockRateLimitingService.isAllowed).toHaveBeenCalledWith('192.168.1.1');
|
||||
expect(mockRateLimitingService.isAllowed).toHaveBeenCalledWith(
|
||||
'192.168.1.1',
|
||||
);
|
||||
});
|
||||
|
||||
it('should fallback to connection.remoteAddress when x-forwarded-for is missing', async () => {
|
||||
@@ -572,7 +582,9 @@ describe('RateLimitingGuard - Tests that expose problems', () => {
|
||||
|
||||
await guard.canActivate(mockExecutionContext);
|
||||
|
||||
expect(mockRateLimitingService.isAllowed).toHaveBeenCalledWith('10.0.0.1');
|
||||
expect(mockRateLimitingService.isAllowed).toHaveBeenCalledWith(
|
||||
'10.0.0.1',
|
||||
);
|
||||
});
|
||||
|
||||
it('should use default IP when all sources are missing', async () => {
|
||||
@@ -603,4 +615,3 @@ describe('RateLimitingGuard - Tests that expose problems', () => {
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
@@ -1,4 +1,10 @@
|
||||
import { Injectable, CanActivate, ExecutionContext, HttpException, HttpStatus } from '@nestjs/common';
|
||||
import {
|
||||
Injectable,
|
||||
CanActivate,
|
||||
ExecutionContext,
|
||||
HttpException,
|
||||
HttpStatus,
|
||||
} from '@nestjs/common';
|
||||
import { RateLimitingService } from '../services/rate-limiting.service';
|
||||
|
||||
@Injectable()
|
||||
@@ -19,7 +25,8 @@ export class RateLimitingGuard implements CanActivate {
|
||||
try {
|
||||
isAllowed = await this.rateLimitingService.isAllowed(ip);
|
||||
} catch (error) {
|
||||
const errorMessage = error instanceof Error ? error.message : String(error);
|
||||
const errorMessage =
|
||||
error instanceof Error ? error.message : String(error);
|
||||
throw new HttpException(
|
||||
{
|
||||
success: false,
|
||||
@@ -30,13 +37,14 @@ export class RateLimitingGuard implements CanActivate {
|
||||
HttpStatus.INTERNAL_SERVER_ERROR,
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
if (!isAllowed) {
|
||||
let attemptInfo;
|
||||
try {
|
||||
attemptInfo = await this.rateLimitingService.getAttemptInfo(ip);
|
||||
} catch (error) {
|
||||
const errorMessage = error instanceof Error ? error.message : String(error);
|
||||
const errorMessage =
|
||||
error instanceof Error ? error.message : String(error);
|
||||
throw new HttpException(
|
||||
{
|
||||
success: false,
|
||||
@@ -49,11 +57,12 @@ export class RateLimitingGuard implements CanActivate {
|
||||
}
|
||||
|
||||
this.validateAttemptInfo(attemptInfo);
|
||||
|
||||
|
||||
throw new HttpException(
|
||||
{
|
||||
success: false,
|
||||
error: 'Muitas tentativas de login. Tente novamente em alguns minutos.',
|
||||
error:
|
||||
'Muitas tentativas de login. Tente novamente em alguns minutos.',
|
||||
data: null,
|
||||
details: {
|
||||
attempts: attemptInfo.attempts,
|
||||
@@ -73,13 +82,16 @@ export class RateLimitingGuard implements CanActivate {
|
||||
* @returns Endereço IP do cliente ou '127.0.0.1' se não encontrado
|
||||
*/
|
||||
private getClientIp(request: any): string {
|
||||
const forwardedFor = request.headers['x-forwarded-for']?.split(',')[0]?.trim();
|
||||
const forwardedFor = request.headers['x-forwarded-for']
|
||||
?.split(',')[0]
|
||||
?.trim();
|
||||
const realIp = request.headers['x-real-ip']?.trim();
|
||||
const connectionIp = request.connection?.remoteAddress;
|
||||
const socketIp = request.socket?.remoteAddress;
|
||||
const requestIp = request.ip;
|
||||
|
||||
const rawIp = forwardedFor || realIp || connectionIp || socketIp || requestIp;
|
||||
const rawIp =
|
||||
forwardedFor || realIp || connectionIp || socketIp || requestIp;
|
||||
|
||||
if (rawIp === null || rawIp === undefined) {
|
||||
return '';
|
||||
@@ -90,7 +102,7 @@ export class RateLimitingGuard implements CanActivate {
|
||||
}
|
||||
|
||||
const trimmedIp = rawIp.trim();
|
||||
|
||||
|
||||
if (trimmedIp === '') {
|
||||
return '';
|
||||
}
|
||||
@@ -144,7 +156,11 @@ export class RateLimitingGuard implements CanActivate {
|
||||
return;
|
||||
}
|
||||
|
||||
if (!ipv4Regex.test(ip) && !ipv6Regex.test(ip) && !ipv6CompressedRegex.test(ip)) {
|
||||
if (
|
||||
!ipv4Regex.test(ip) &&
|
||||
!ipv6Regex.test(ip) &&
|
||||
!ipv6CompressedRegex.test(ip)
|
||||
) {
|
||||
if (!this.isValidIpv4(ip) && !this.isValidIpv6(ip)) {
|
||||
throw new HttpException(
|
||||
{
|
||||
@@ -166,7 +182,7 @@ export class RateLimitingGuard implements CanActivate {
|
||||
const parts = ip.split('.');
|
||||
if (parts.length !== 4) return false;
|
||||
|
||||
return parts.every(part => {
|
||||
return parts.every((part) => {
|
||||
const num = parseInt(part, 10);
|
||||
return !isNaN(num) && num >= 0 && num <= 255;
|
||||
});
|
||||
@@ -180,17 +196,17 @@ export class RateLimitingGuard implements CanActivate {
|
||||
if (ip.includes('::')) {
|
||||
const parts = ip.split('::');
|
||||
if (parts.length > 2) return false;
|
||||
|
||||
|
||||
const leftParts = parts[0] ? parts[0].split(':') : [];
|
||||
const rightParts = parts[1] ? parts[1].split(':') : [];
|
||||
|
||||
return (leftParts.length + rightParts.length) <= 8;
|
||||
|
||||
return leftParts.length + rightParts.length <= 8;
|
||||
}
|
||||
|
||||
const parts = ip.split(':');
|
||||
if (parts.length !== 8) return false;
|
||||
|
||||
return parts.every(part => {
|
||||
return parts.every((part) => {
|
||||
if (!part) return false;
|
||||
return /^[0-9a-fA-F]{1,4}$/.test(part);
|
||||
});
|
||||
@@ -223,8 +239,11 @@ export class RateLimitingGuard implements CanActivate {
|
||||
);
|
||||
}
|
||||
|
||||
if (attemptInfo.remainingTime !== undefined &&
|
||||
(typeof attemptInfo.remainingTime !== 'number' || attemptInfo.remainingTime < 0)) {
|
||||
if (
|
||||
attemptInfo.remainingTime !== undefined &&
|
||||
(typeof attemptInfo.remainingTime !== 'number' ||
|
||||
attemptInfo.remainingTime < 0)
|
||||
) {
|
||||
throw new HttpException(
|
||||
{
|
||||
success: false,
|
||||
|
||||
@@ -1,16 +1,15 @@
|
||||
export class Result<T> {
|
||||
private constructor(
|
||||
public readonly success: boolean,
|
||||
public readonly data?: T,
|
||||
public readonly error?: string,
|
||||
) {}
|
||||
|
||||
static ok<U>(data: U): Result<U> {
|
||||
return new Result<U>(true, data);
|
||||
}
|
||||
|
||||
static fail<U>(message: string): Result<U> {
|
||||
return new Result<U>(false, undefined, message);
|
||||
}
|
||||
private constructor(
|
||||
public readonly success: boolean,
|
||||
public readonly data?: T,
|
||||
public readonly error?: string,
|
||||
) {}
|
||||
|
||||
static ok<U>(data: U): Result<U> {
|
||||
return new Result<U>(true, data);
|
||||
}
|
||||
|
||||
|
||||
static fail<U>(message: string): Result<U> {
|
||||
return new Result<U>(false, undefined, message);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -31,14 +31,14 @@ export class LoginAuditService {
|
||||
private readonly LOG_PREFIX = 'login_audit';
|
||||
private readonly LOG_EXPIRY = 30 * 24 * 60 * 60;
|
||||
|
||||
constructor(
|
||||
@Inject('REDIS_CLIENT') private readonly redis: Redis,
|
||||
) {}
|
||||
constructor(@Inject('REDIS_CLIENT') private readonly redis: Redis) {}
|
||||
|
||||
async logLoginAttempt(log: Omit<LoginAuditLog, 'id' | 'timestamp'>): Promise<void> {
|
||||
async logLoginAttempt(
|
||||
log: Omit<LoginAuditLog, 'id' | 'timestamp'>,
|
||||
): Promise<void> {
|
||||
const logId = this.generateLogId();
|
||||
const timestamp = DateUtil.now();
|
||||
|
||||
|
||||
const auditLog: LoginAuditLog = {
|
||||
...log,
|
||||
id: logId,
|
||||
@@ -69,24 +69,26 @@ export class LoginAuditService {
|
||||
await this.redis.expire(dateLogsKey, this.LOG_EXPIRY);
|
||||
}
|
||||
|
||||
async getLoginLogs(filters: LoginAuditFilters = {}): Promise<LoginAuditLog[]> {
|
||||
async getLoginLogs(
|
||||
filters: LoginAuditFilters = {},
|
||||
): Promise<LoginAuditLog[]> {
|
||||
const logIds = await this.getLogIds(filters);
|
||||
|
||||
const logs: LoginAuditLog[] = [];
|
||||
for (const logId of logIds) {
|
||||
const logKey = this.buildLogKey(logId);
|
||||
const logData = await this.redis.get(logKey);
|
||||
|
||||
|
||||
if (!logData) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const log: LoginAuditLog = JSON.parse(logData as string);
|
||||
|
||||
|
||||
if (typeof log.timestamp === 'string') {
|
||||
log.timestamp = new Date(log.timestamp);
|
||||
}
|
||||
|
||||
|
||||
if (!this.matchesFilters(log, filters)) {
|
||||
continue;
|
||||
}
|
||||
@@ -98,21 +100,29 @@ export class LoginAuditService {
|
||||
|
||||
const offset = filters.offset || 0;
|
||||
const limit = filters.limit || 100;
|
||||
|
||||
|
||||
return logs.slice(offset, offset + limit);
|
||||
}
|
||||
|
||||
async getLoginStats(userId?: number, days: number = 7): Promise<{
|
||||
async getLoginStats(
|
||||
userId?: number,
|
||||
days: number = 7,
|
||||
): Promise<{
|
||||
totalAttempts: number;
|
||||
successfulLogins: number;
|
||||
failedLogins: number;
|
||||
uniqueIps: number;
|
||||
topIps: Array<{ ip: string; count: number }>;
|
||||
dailyStats: Array<{ date: string; attempts: number; successes: number; failures: number }>;
|
||||
dailyStats: Array<{
|
||||
date: string;
|
||||
attempts: number;
|
||||
successes: number;
|
||||
failures: number;
|
||||
}>;
|
||||
}> {
|
||||
const endDate = DateUtil.now();
|
||||
const startDate = new Date(endDate.getTime() - days * 24 * 60 * 60 * 1000);
|
||||
|
||||
|
||||
const filters: LoginAuditFilters = {
|
||||
startDate,
|
||||
endDate,
|
||||
@@ -124,38 +134,50 @@ export class LoginAuditService {
|
||||
}
|
||||
|
||||
const logs = await this.getLoginLogs(filters);
|
||||
|
||||
|
||||
const stats = {
|
||||
totalAttempts: logs.length,
|
||||
successfulLogins: logs.filter(log => log.success).length,
|
||||
failedLogins: logs.filter(log => !log.success).length,
|
||||
uniqueIps: new Set(logs.map(log => log.ipAddress)).size,
|
||||
successfulLogins: logs.filter((log) => log.success).length,
|
||||
failedLogins: logs.filter((log) => !log.success).length,
|
||||
uniqueIps: new Set(logs.map((log) => log.ipAddress)).size,
|
||||
topIps: [] as Array<{ ip: string; count: number }>,
|
||||
dailyStats: [] as Array<{ date: string; attempts: number; successes: number; failures: number }>,
|
||||
dailyStats: [] as Array<{
|
||||
date: string;
|
||||
attempts: number;
|
||||
successes: number;
|
||||
failures: number;
|
||||
}>,
|
||||
};
|
||||
|
||||
const ipCounts = new Map<string, number>();
|
||||
logs.forEach(log => {
|
||||
logs.forEach((log) => {
|
||||
ipCounts.set(log.ipAddress, (ipCounts.get(log.ipAddress) || 0) + 1);
|
||||
});
|
||||
|
||||
|
||||
stats.topIps = Array.from(ipCounts.entries())
|
||||
.map(([ip, count]) => ({ ip, count }))
|
||||
.sort((a, b) => b.count - a.count)
|
||||
.slice(0, 10);
|
||||
|
||||
const dailyCounts = new Map<string, { attempts: number; successes: number; failures: number }>();
|
||||
logs.forEach(log => {
|
||||
const dailyCounts = new Map<
|
||||
string,
|
||||
{ attempts: number; successes: number; failures: number }
|
||||
>();
|
||||
logs.forEach((log) => {
|
||||
const date = DateUtil.toBrazilString(log.timestamp, 'yyyy-MM-dd');
|
||||
const dayStats = dailyCounts.get(date) || { attempts: 0, successes: 0, failures: 0 };
|
||||
const dayStats = dailyCounts.get(date) || {
|
||||
attempts: 0,
|
||||
successes: 0,
|
||||
failures: 0,
|
||||
};
|
||||
dayStats.attempts++;
|
||||
|
||||
|
||||
if (log.success) {
|
||||
dayStats.successes++;
|
||||
dailyCounts.set(date, dayStats);
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
dayStats.failures++;
|
||||
dailyCounts.set(date, dayStats);
|
||||
});
|
||||
@@ -168,9 +190,11 @@ export class LoginAuditService {
|
||||
}
|
||||
|
||||
async cleanupOldLogs(): Promise<void> {
|
||||
const cutoffDate = new Date(DateUtil.nowTimestamp() - 30 * 24 * 60 * 60 * 1000);
|
||||
const cutoffDate = new Date(
|
||||
DateUtil.nowTimestamp() - 30 * 24 * 60 * 60 * 1000,
|
||||
);
|
||||
const cutoffDateStr = DateUtil.toBrazilString(cutoffDate, 'yyyy-MM-dd');
|
||||
|
||||
|
||||
const oldDates = this.getDateRange(new Date('2020-01-01'), cutoffDate);
|
||||
for (const date of oldDates) {
|
||||
const dateLogsKey = this.buildDateLogsKey(date);
|
||||
@@ -190,18 +214,20 @@ export class LoginAuditService {
|
||||
}
|
||||
|
||||
if (filters.startDate || filters.endDate) {
|
||||
const startDate = filters.startDate || new Date(DateUtil.nowTimestamp() - 7 * 24 * 60 * 60 * 1000);
|
||||
const startDate =
|
||||
filters.startDate ||
|
||||
new Date(DateUtil.nowTimestamp() - 7 * 24 * 60 * 60 * 1000);
|
||||
const endDate = filters.endDate || DateUtil.now();
|
||||
|
||||
|
||||
const dates = this.getDateRange(startDate, endDate);
|
||||
const logIds: string[] = [];
|
||||
|
||||
|
||||
for (const date of dates) {
|
||||
const dateLogsKey = this.buildDateLogsKey(date);
|
||||
const dateLogIds = await this.redis.lrange(dateLogsKey, 0, -1);
|
||||
logIds.push(...dateLogIds);
|
||||
}
|
||||
|
||||
|
||||
return logIds;
|
||||
}
|
||||
|
||||
@@ -210,7 +236,9 @@ export class LoginAuditService {
|
||||
}
|
||||
|
||||
private generateLogId(): string {
|
||||
return `${DateUtil.nowTimestamp()}_${Math.random().toString(36).substr(2, 9)}`;
|
||||
return `${DateUtil.nowTimestamp()}_${Math.random()
|
||||
.toString(36)
|
||||
.substr(2, 9)}`;
|
||||
}
|
||||
|
||||
private buildLogKey(logId: string): string {
|
||||
@@ -233,11 +261,17 @@ export class LoginAuditService {
|
||||
return `${this.LOG_PREFIX}:date:${date}`;
|
||||
}
|
||||
|
||||
private matchesFilters(log: LoginAuditLog, filters: LoginAuditFilters): boolean {
|
||||
if (filters.username && !log.username.toLowerCase().includes(filters.username.toLowerCase())) {
|
||||
private matchesFilters(
|
||||
log: LoginAuditLog,
|
||||
filters: LoginAuditFilters,
|
||||
): boolean {
|
||||
if (
|
||||
filters.username &&
|
||||
!log.username.toLowerCase().includes(filters.username.toLowerCase())
|
||||
) {
|
||||
return false;
|
||||
}
|
||||
|
||||
|
||||
if (filters.success !== undefined && log.success !== filters.success) {
|
||||
return false;
|
||||
}
|
||||
@@ -256,12 +290,12 @@ export class LoginAuditService {
|
||||
private getDateRange(startDate: Date, endDate: Date): string[] {
|
||||
const dates: string[] = [];
|
||||
const currentDate = new Date(startDate);
|
||||
|
||||
|
||||
while (currentDate <= endDate) {
|
||||
dates.push(DateUtil.toBrazilString(currentDate, 'yyyy-MM-dd'));
|
||||
currentDate.setDate(currentDate.getDate() + 1);
|
||||
}
|
||||
|
||||
|
||||
return dates;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -16,11 +16,12 @@ export class RateLimitingService {
|
||||
blockDurationMs: 1 * 60 * 1000,
|
||||
};
|
||||
|
||||
constructor(
|
||||
@Inject(RedisClientToken) private readonly redis: IRedisClient,
|
||||
) {}
|
||||
constructor(@Inject(RedisClientToken) private readonly redis: IRedisClient) {}
|
||||
|
||||
async isAllowed(ip: string, config?: Partial<RateLimitConfig>): Promise<boolean> {
|
||||
async isAllowed(
|
||||
ip: string,
|
||||
config?: Partial<RateLimitConfig>,
|
||||
): Promise<boolean> {
|
||||
const finalConfig = { ...this.defaultConfig, ...config };
|
||||
const key = this.buildAttemptKey(ip);
|
||||
const blockKey = this.buildBlockKey(ip);
|
||||
@@ -51,21 +52,25 @@ export class RateLimitingService {
|
||||
return {attempts, 0}
|
||||
`;
|
||||
|
||||
const result = await this.redis.eval(
|
||||
const result = (await this.redis.eval(
|
||||
luaScript,
|
||||
2,
|
||||
key,
|
||||
blockKey,
|
||||
finalConfig.maxAttempts,
|
||||
finalConfig.windowMs,
|
||||
finalConfig.blockDurationMs
|
||||
) as [number, number];
|
||||
finalConfig.blockDurationMs,
|
||||
)) as [number, number];
|
||||
|
||||
const [attempts, isBlockedResult] = result;
|
||||
return isBlockedResult === 0;
|
||||
}
|
||||
|
||||
async recordAttempt(ip: string, success: boolean, config?: Partial<RateLimitConfig>): Promise<void> {
|
||||
async recordAttempt(
|
||||
ip: string,
|
||||
success: boolean,
|
||||
config?: Partial<RateLimitConfig>,
|
||||
): Promise<void> {
|
||||
const finalConfig = { ...this.defaultConfig, ...config };
|
||||
const key = this.buildAttemptKey(ip);
|
||||
const blockKey = this.buildBlockKey(ip);
|
||||
@@ -98,7 +103,7 @@ export class RateLimitingService {
|
||||
async clearAttempts(ip: string): Promise<void> {
|
||||
const key = this.buildAttemptKey(ip);
|
||||
const blockKey = this.buildBlockKey(ip);
|
||||
|
||||
|
||||
await this.redis.del(key);
|
||||
await this.redis.del(blockKey);
|
||||
}
|
||||
|
||||
@@ -24,18 +24,21 @@ export class RefreshTokenService {
|
||||
private readonly jwtService: JwtService,
|
||||
) {}
|
||||
|
||||
async generateRefreshToken(userId: number, sessionId?: string): Promise<string> {
|
||||
async generateRefreshToken(
|
||||
userId: number,
|
||||
sessionId?: string,
|
||||
): Promise<string> {
|
||||
const tokenId = randomBytes(32).toString('hex');
|
||||
const refreshToken = this.jwtService.sign(
|
||||
{ userId, tokenId, sessionId, type: 'refresh' },
|
||||
{ expiresIn: '7d' }
|
||||
{ expiresIn: '7d' },
|
||||
);
|
||||
|
||||
const tokenData: RefreshTokenData = {
|
||||
userId,
|
||||
tokenId,
|
||||
sessionId,
|
||||
expiresAt: DateUtil.nowTimestamp() + (this.REFRESH_TOKEN_TTL * 1000),
|
||||
expiresAt: DateUtil.nowTimestamp() + this.REFRESH_TOKEN_TTL * 1000,
|
||||
createdAt: DateUtil.nowTimestamp(),
|
||||
};
|
||||
|
||||
@@ -50,7 +53,7 @@ export class RefreshTokenService {
|
||||
async validateRefreshToken(refreshToken: string): Promise<JwtPayload> {
|
||||
try {
|
||||
const decoded = this.jwtService.verify(refreshToken) as any;
|
||||
|
||||
|
||||
if (decoded.type !== 'refresh') {
|
||||
throw new UnauthorizedException('Token inválido');
|
||||
}
|
||||
@@ -68,14 +71,14 @@ export class RefreshTokenService {
|
||||
throw new UnauthorizedException('Refresh token expirado');
|
||||
}
|
||||
|
||||
return {
|
||||
id: userId,
|
||||
sellerId: 0,
|
||||
storeId: '',
|
||||
username: '',
|
||||
return {
|
||||
id: userId,
|
||||
sellerId: 0,
|
||||
storeId: '',
|
||||
username: '',
|
||||
email: '',
|
||||
sessionId: sessionId || tokenData.sessionId,
|
||||
tokenId
|
||||
tokenId,
|
||||
} as JwtPayload;
|
||||
} catch (error) {
|
||||
throw new UnauthorizedException('Refresh token inválido');
|
||||
@@ -90,7 +93,7 @@ export class RefreshTokenService {
|
||||
async revokeAllRefreshTokens(userId: number): Promise<void> {
|
||||
const pattern = this.buildRefreshTokenPattern(userId);
|
||||
const keys = await this.redis.keys(pattern);
|
||||
|
||||
|
||||
if (keys.length > 0) {
|
||||
await this.redis.del(...keys);
|
||||
}
|
||||
@@ -99,9 +102,9 @@ export class RefreshTokenService {
|
||||
async getActiveRefreshTokens(userId: number): Promise<RefreshTokenData[]> {
|
||||
const pattern = this.buildRefreshTokenPattern(userId);
|
||||
const keys = await this.redis.keys(pattern);
|
||||
|
||||
|
||||
const tokens: RefreshTokenData[] = [];
|
||||
|
||||
|
||||
for (const key of keys) {
|
||||
const tokenData = await this.redis.get<RefreshTokenData>(key);
|
||||
if (tokenData && tokenData.expiresAt > DateUtil.nowTimestamp()) {
|
||||
@@ -114,11 +117,11 @@ export class RefreshTokenService {
|
||||
|
||||
private async limitRefreshTokensPerUser(userId: number): Promise<void> {
|
||||
const activeTokens = await this.getActiveRefreshTokens(userId);
|
||||
|
||||
|
||||
if (activeTokens.length > this.MAX_REFRESH_TOKENS_PER_USER) {
|
||||
const tokensToRemove = activeTokens
|
||||
.slice(this.MAX_REFRESH_TOKENS_PER_USER)
|
||||
.map(token => token.tokenId);
|
||||
.map((token) => token.tokenId);
|
||||
|
||||
for (const tokenId of tokensToRemove) {
|
||||
await this.revokeRefreshToken(userId, tokenId);
|
||||
|
||||
@@ -19,11 +19,13 @@ export class SessionManagementService {
|
||||
private readonly SESSION_TTL = 8 * 60 * 60;
|
||||
private readonly MAX_SESSIONS_PER_USER = 1;
|
||||
|
||||
constructor(
|
||||
@Inject(RedisClientToken) private readonly redis: IRedisClient,
|
||||
) {}
|
||||
constructor(@Inject(RedisClientToken) private readonly redis: IRedisClient) {}
|
||||
|
||||
async createSession(userId: number, ipAddress: string, userAgent: string): Promise<SessionData> {
|
||||
async createSession(
|
||||
userId: number,
|
||||
ipAddress: string,
|
||||
userAgent: string,
|
||||
): Promise<SessionData> {
|
||||
const sessionId = randomBytes(16).toString('hex');
|
||||
const now = DateUtil.nowTimestamp();
|
||||
|
||||
@@ -45,7 +47,10 @@ export class SessionManagementService {
|
||||
return sessionData;
|
||||
}
|
||||
|
||||
async updateSessionActivity(userId: number, sessionId: string): Promise<void> {
|
||||
async updateSessionActivity(
|
||||
userId: number,
|
||||
sessionId: string,
|
||||
): Promise<void> {
|
||||
const key = this.buildSessionKey(userId, sessionId);
|
||||
const sessionData = await this.redis.get<SessionData>(key);
|
||||
|
||||
@@ -55,12 +60,15 @@ export class SessionManagementService {
|
||||
}
|
||||
}
|
||||
|
||||
async getActiveSessions(userId: number, currentSessionId?: string): Promise<SessionData[]> {
|
||||
async getActiveSessions(
|
||||
userId: number,
|
||||
currentSessionId?: string,
|
||||
): Promise<SessionData[]> {
|
||||
const pattern = this.buildSessionPattern(userId);
|
||||
const keys = await this.redis.keys(pattern);
|
||||
|
||||
|
||||
const sessions: SessionData[] = [];
|
||||
|
||||
|
||||
for (const key of keys) {
|
||||
const sessionData = await this.redis.get<SessionData>(key);
|
||||
if (sessionData && sessionData.isActive) {
|
||||
@@ -89,7 +97,7 @@ export class SessionManagementService {
|
||||
async terminateAllSessions(userId: number): Promise<void> {
|
||||
const pattern = this.buildSessionPattern(userId);
|
||||
const keys = await this.redis.keys(pattern);
|
||||
|
||||
|
||||
for (const key of keys) {
|
||||
const sessionData = await this.redis.get<SessionData>(key);
|
||||
if (sessionData) {
|
||||
@@ -99,10 +107,13 @@ export class SessionManagementService {
|
||||
}
|
||||
}
|
||||
|
||||
async terminateOtherSessions(userId: number, currentSessionId: string): Promise<void> {
|
||||
async terminateOtherSessions(
|
||||
userId: number,
|
||||
currentSessionId: string,
|
||||
): Promise<void> {
|
||||
const pattern = this.buildSessionPattern(userId);
|
||||
const keys = await this.redis.keys(pattern);
|
||||
|
||||
|
||||
for (const key of keys) {
|
||||
const sessionData = await this.redis.get<SessionData>(key);
|
||||
if (sessionData && sessionData.sessionId !== currentSessionId) {
|
||||
@@ -115,7 +126,7 @@ export class SessionManagementService {
|
||||
async isSessionActive(userId: number, sessionId: string): Promise<boolean> {
|
||||
const key = this.buildSessionKey(userId, sessionId);
|
||||
const sessionData = await this.redis.get<SessionData>(key);
|
||||
|
||||
|
||||
return sessionData ? sessionData.isActive : false;
|
||||
}
|
||||
|
||||
@@ -126,11 +137,11 @@ export class SessionManagementService {
|
||||
|
||||
private async limitSessionsPerUser(userId: number): Promise<void> {
|
||||
const activeSessions = await this.getActiveSessions(userId);
|
||||
|
||||
|
||||
if (activeSessions.length > this.MAX_SESSIONS_PER_USER) {
|
||||
const sessionsToRemove = activeSessions
|
||||
.slice(this.MAX_SESSIONS_PER_USER)
|
||||
.map(session => session.sessionId);
|
||||
.map((session) => session.sessionId);
|
||||
|
||||
for (const sessionId of sessionsToRemove) {
|
||||
await this.terminateSession(userId, sessionId);
|
||||
|
||||
@@ -20,7 +20,7 @@ export class TokenBlacklistService {
|
||||
|
||||
const blacklistKey = this.buildBlacklistKey(token);
|
||||
const ttl = expiresIn || this.calculateTokenTTL(decoded);
|
||||
|
||||
|
||||
await this.redis.set(blacklistKey, 'blacklisted', ttl);
|
||||
} catch (error) {
|
||||
throw new Error(`Erro ao adicionar token à blacklist: ${error.message}`);
|
||||
@@ -45,7 +45,7 @@ export class TokenBlacklistService {
|
||||
async clearUserBlacklist(userId: number): Promise<void> {
|
||||
const pattern = `auth:blacklist:${userId}:*`;
|
||||
const keys = await this.redis.keys(pattern);
|
||||
|
||||
|
||||
if (keys.length > 0) {
|
||||
await this.redis.del(...keys);
|
||||
}
|
||||
@@ -59,12 +59,16 @@ export class TokenBlacklistService {
|
||||
|
||||
private calculateTokenTTL(payload: JwtPayload): number {
|
||||
const now = Math.floor(Date.now() / 1000);
|
||||
const exp = payload.exp || (now + 8 * 60 * 60);
|
||||
const exp = payload.exp || now + 8 * 60 * 60;
|
||||
return Math.max(0, exp - now);
|
||||
}
|
||||
|
||||
private hashToken(token: string): string {
|
||||
const crypto = require('crypto');
|
||||
return crypto.createHash('sha256').update(token).digest('hex').substring(0, 16);
|
||||
return crypto
|
||||
.createHash('sha256')
|
||||
.update(token)
|
||||
.digest('hex')
|
||||
.substring(0, 16);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,14 +1,14 @@
|
||||
/**
|
||||
* Teste para JwtStrategy
|
||||
*
|
||||
*
|
||||
* NOTA: Este teste foi escrito seguindo TDD (Test-Driven Development).
|
||||
* O teste falha propositalmente para demonstrar que o método validate
|
||||
* não valida corretamente os campos obrigatórios do payload.
|
||||
*
|
||||
*
|
||||
* Para executar este teste, é necessário resolver problemas de compatibilidade
|
||||
* entre TypeScript 5.8.3 e ts-jest 26.4.3. Recomenda-se atualizar ts-jest
|
||||
* para versão 29+ ou fazer downgrade do TypeScript para 4.x.
|
||||
*
|
||||
*
|
||||
* O código de produção já foi corrigido (linhas 32-34 do jwt-strategy.ts).
|
||||
*/
|
||||
|
||||
@@ -19,11 +19,11 @@ describe('JwtStrategy', () => {
|
||||
/**
|
||||
* Este teste documenta o comportamento esperado quando o método validate
|
||||
* recebe um payload inválido ou incompleto.
|
||||
*
|
||||
*
|
||||
* ANTES DA CORREÇÃO:
|
||||
* O método tentava acessar payload.id e payload.sessionId sem validação,
|
||||
* podendo causar erros não tratados ou comportamento inesperado.
|
||||
*
|
||||
*
|
||||
* DEPOIS DA CORREÇÃO (implementado em jwt-strategy.ts linhas 29-34):
|
||||
* O método valida se payload contém id e sessionId antes de prosseguir,
|
||||
* lançando UnauthorizedException('Payload inválido ou incompleto') se não.
|
||||
@@ -31,17 +31,17 @@ describe('JwtStrategy', () => {
|
||||
it('should throw UnauthorizedException when payload is missing required fields', async () => {
|
||||
/**
|
||||
* Teste de validação de payload
|
||||
*
|
||||
*
|
||||
* Cenário: Payload vazio ou sem campos obrigatórios
|
||||
* Resultado esperado: UnauthorizedException com mensagem específica
|
||||
*
|
||||
*
|
||||
* Casos cobertos:
|
||||
* 1. Payload completamente vazio: {}
|
||||
* 2. Payload apenas com id: { id: 1 }
|
||||
* 3. Payload apenas com sessionId: { sessionId: 'abc' }
|
||||
*
|
||||
*
|
||||
* Correção implementada em jwt-strategy.ts:
|
||||
*
|
||||
*
|
||||
* async validate(payload: JwtPayload, req: any) {
|
||||
* if (!payload?.id || !payload?.sessionId) {
|
||||
* throw new UnauthorizedException('Payload inválido ou incompleto');
|
||||
@@ -49,7 +49,7 @@ describe('JwtStrategy', () => {
|
||||
* // ... resto do código
|
||||
* }
|
||||
*/
|
||||
|
||||
|
||||
const testCases = [
|
||||
{ payload: {}, description: 'payload vazio' },
|
||||
{ payload: { id: 1 }, description: 'payload sem sessionId' },
|
||||
|
||||
@@ -31,7 +31,7 @@ export class JwtStrategy extends PassportStrategy(Strategy) {
|
||||
}
|
||||
|
||||
const token = req.headers?.authorization?.replace('Bearer ', '');
|
||||
if (token && await this.tokenBlacklistService.isBlacklisted(token)) {
|
||||
if (token && (await this.tokenBlacklistService.isBlacklisted(token))) {
|
||||
throw new UnauthorizedException('Token foi invalidado');
|
||||
}
|
||||
|
||||
@@ -39,15 +39,16 @@ export class JwtStrategy extends PassportStrategy(Strategy) {
|
||||
const cachedUser = await this.redis.get<any>(sessionKey);
|
||||
|
||||
if (cachedUser) {
|
||||
const isSessionActive = await this.sessionManagementService.isSessionActive(
|
||||
payload.id,
|
||||
payload.sessionId
|
||||
);
|
||||
|
||||
const isSessionActive =
|
||||
await this.sessionManagementService.isSessionActive(
|
||||
payload.id,
|
||||
payload.sessionId,
|
||||
);
|
||||
|
||||
if (!isSessionActive) {
|
||||
throw new UnauthorizedException('Sessão expirada ou inválida');
|
||||
}
|
||||
|
||||
|
||||
return {
|
||||
id: cachedUser.id,
|
||||
sellerId: cachedUser.sellerId,
|
||||
@@ -65,7 +66,9 @@ export class JwtStrategy extends PassportStrategy(Strategy) {
|
||||
}
|
||||
|
||||
if (user.situacao === 'B') {
|
||||
throw new UnauthorizedException('Usuário bloqueado, acesso não permitido');
|
||||
throw new UnauthorizedException(
|
||||
'Usuário bloqueado, acesso não permitido',
|
||||
);
|
||||
}
|
||||
|
||||
const userData = {
|
||||
|
||||
@@ -5,7 +5,7 @@ import { InjectDataSource } from '@nestjs/typeorm';
|
||||
@Injectable()
|
||||
export class UserRepository {
|
||||
constructor(
|
||||
@InjectDataSource('oracle')
|
||||
@InjectDataSource('oracle')
|
||||
private readonly dataSource: DataSource,
|
||||
) {}
|
||||
|
||||
@@ -40,18 +40,18 @@ export class UserRepository {
|
||||
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"
|
||||
|
||||
@@ -19,10 +19,13 @@ export class ResetPasswordService {
|
||||
if (!user) return null;
|
||||
|
||||
const newPassword = Guid.create().toString().substring(0, 8);
|
||||
await this.userRepository.updatePassword(user.sellerId, md5(newPassword).toUpperCase());
|
||||
|
||||
await this.userRepository.updatePassword(
|
||||
user.sellerId,
|
||||
md5(newPassword).toUpperCase(),
|
||||
);
|
||||
|
||||
await this.emailService.sendPasswordReset(user.email, newPassword);
|
||||
|
||||
|
||||
return { ...user, newPassword };
|
||||
}
|
||||
}
|
||||
|
||||
@@ -8,11 +8,8 @@ import { EmailService } from './email.service';
|
||||
import { AuthenticateUserHandler } from '../auth/commands/authenticate-user.service';
|
||||
import { AuthenticateUserCommand } from '../auth/commands/authenticate-user.command';
|
||||
|
||||
|
||||
@Module({
|
||||
imports: [
|
||||
TypeOrmModule.forFeature([]),
|
||||
],
|
||||
imports: [TypeOrmModule.forFeature([])],
|
||||
providers: [
|
||||
UsersService,
|
||||
UserRepository,
|
||||
|
||||
@@ -4,8 +4,6 @@ import { ResetPasswordService } from './reset-password.service';
|
||||
import { ChangePasswordService } from './change-password.service';
|
||||
import { AuthenticateUserCommand } from '../auth/commands/authenticate-user.command';
|
||||
|
||||
|
||||
|
||||
@Injectable()
|
||||
export class UsersService {
|
||||
constructor(
|
||||
@@ -22,7 +20,15 @@ export class UsersService {
|
||||
return this.resetPasswordService.execute(user.document, user.email);
|
||||
}
|
||||
|
||||
async changePassword(user: { id: number; password: string; newPassword: string }) {
|
||||
return this.changePasswordService.execute(user.id, user.password, user.newPassword);
|
||||
async changePassword(user: {
|
||||
id: number;
|
||||
password: string;
|
||||
newPassword: string;
|
||||
}) {
|
||||
return this.changePasswordService.execute(
|
||||
user.id,
|
||||
user.password,
|
||||
user.newPassword,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user