diff --git a/src/components/LoginForm.tsx b/src/components/LoginForm.tsx index 47007de..916decc 100644 --- a/src/components/LoginForm.tsx +++ b/src/components/LoginForm.tsx @@ -107,7 +107,7 @@ export function LoginForm({ const responseBody = await response.json() - if (response.ok) { + if (response.status === 201) { showToast({ message: 'Usuário logado com sucesso!', duration: 3000, @@ -123,6 +123,22 @@ export function LoginForm({ return } + if (response.status === 200) { + showToast({ + message: 'Autenticação de dois fatores necessária.', + duration: 3000, + variant: 'info', + closeButton: false, + redirect: { + path: `${webserver.host}/verify-two-factor-opt?token=${responseBody.userId}`, + countdownSeconds: 2, + }, + }) + + form.reset() + return + } + if (response.status === 404 || response.status === 401) { showToast({ message: 'Usuário ou Senha incorretos.', diff --git a/src/tests/integration/api/v1/public/auth/login/credential/post.test.ts b/src/tests/integration/api/v1/public/auth/login/credential/post.test.ts index c366bff..de40fd6 100644 --- a/src/tests/integration/api/v1/public/auth/login/credential/post.test.ts +++ b/src/tests/integration/api/v1/public/auth/login/credential/post.test.ts @@ -46,7 +46,7 @@ describe('POST /api/v1/public/auth/login/credential', () => { test('should return 403 if email not verified', async () => { const user = await utilsTest.createDefaultUser() // this user is not email verified - const password = 'Password23@#!' + const password = 'Password123$%$' const device = 'device-id' const response = await fetch( @@ -228,6 +228,35 @@ describe('POST /api/v1/public/auth/login/credential', () => { expect(diffDays).toBe(30) }) + test('should return 200 and send two-factor token when user has two_factor_enabled true', async () => { + const user = await utilsTest.createDefaultUserTwoFactor() + + const password = 'Password123$%$' + const device = 'device-id' + + const response = await fetch( + `${webserver.host}/api/v1/public/auth/login/credential`, + { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + }, + body: JSON.stringify({ + email: user.email, + password, + device, + }), + }, + ) + + const responseBody = await response.json() + + expect(response.status).toBe(200) + expect(responseBody.message).toBe( + 'Token de autenticação de dois fatores enviado com sucesso!', + ) + }) + test('should delete all expired sessions', async () => { // Create 10 sessions with expired tokens for (let i = 0; i < 10; i++) { diff --git a/src/tests/integration/api/v1/public/auth/verify-email-opt/get.test.ts b/src/tests/integration/api/v1/public/auth/verify-email-opt/get.test.ts index ec09e35..e390c6c 100644 --- a/src/tests/integration/api/v1/public/auth/verify-email-opt/get.test.ts +++ b/src/tests/integration/api/v1/public/auth/verify-email-opt/get.test.ts @@ -96,7 +96,7 @@ describe('GET /api/v1/public/auth/verify-email-opt', () => { expect(user.emailVerified).toBeDefined() expect(user.email_verified_provider).toBe('credential') - expect(user.profile_completion_score).toBe(4) + expect(user.profile_completion_score).toBe(3) const tokenResult = await database.query({ text: ` diff --git a/src/use-cases/auth/login/credential/login-credential.ts b/src/use-cases/auth/login/credential/login-credential.ts index 3f4e500..5c90d02 100644 --- a/src/use-cases/auth/login/credential/login-credential.ts +++ b/src/use-cases/auth/login/credential/login-credential.ts @@ -2,7 +2,9 @@ import { comparePassword } from '@/lib/bcrypt' import { CookieRepository } from '@/repositories/cookie-repository' import { SessionRepository } from '@/repositories/session-repository' import { UserRepository } from '@/repositories/user-repository' +import { VerificationTokenRepository } from '@/repositories/verification-token-repository' import { v4 } from 'uuid' +import { generateAndSendTwoFactorToken } from './two-factor/send-token/two-factor-send-token' interface LoginCredentialUseCaseRequest { email: string @@ -25,6 +27,7 @@ export class LoginCredentialUseCase { constructor( private userRepository: UserRepository, private sessionRepository: SessionRepository, + private verificationTokenRepository: VerificationTokenRepository, private cookieRepository: CookieRepository, ) {} @@ -46,16 +49,7 @@ export class LoginCredentialUseCase { } } - // 3. useCase - check if email was verified - if (!user.emailVerified) { - return { - status: 403, - message: 'Email não verificado.', - userId: user.id, - } - } - - // 4. useCase - check if password is correct + // 3. useCase - check if password is correct const isPasswordCorrect = await comparePassword( password, user.password_hash as string, @@ -68,41 +62,67 @@ export class LoginCredentialUseCase { } } - // 5. useCase - create session token - const sessionToken = v4() - const sessionExpiry = new Date(Date.now() + DAYS_30_IN_MILLISECONDS) - - // 6. useCase - check if exist a session with userId and device, if exist delete the session and create new other, if not create a new session - const sessionExists = - await this.sessionRepository.getSessionByUserIdAndDevice( - user.id, - device || '', - ) - - if (sessionExists) { - await this.sessionRepository.deleteSessionByToken( - sessionExists.sessionToken, - ) - } + // 4. useCase - check if user is two factor enabled + if (user.two_factor_enabled) { + // 5. useCase - Generate and send two-factor token + await generateAndSendTwoFactorToken({ + userId: user.id, + userEmail: user.email, + verificationTokenRepository: this.verificationTokenRepository, + tokenType: 'TWO_FACTOR_VERIFICATION', + }) + + return { + status: 200, + message: 'Token de autenticação de dois fatores enviado com sucesso!', + userId: user.id, + } + } else { + // 5. useCase - check if email was verified + if (!user.emailVerified) { + return { + status: 403, + message: 'Email não verificado.', + userId: user.id, + } + } + + // 6. useCase - create session token + const sessionToken = v4() + const sessionExpiry = new Date(Date.now() + DAYS_30_IN_MILLISECONDS) + + // 7. useCase - check if exist a session with userId and device, if exist delete the session and create new other, if not create a new session + const sessionExists = + await this.sessionRepository.getSessionByUserIdAndDevice( + user.id, + device || '', + ) + + if (sessionExists) { + await this.sessionRepository.deleteSessionByToken( + sessionExists.sessionToken, + ) + } + + await this.sessionRepository.createSession({ + sessionToken, + userId: user.id, + expires: sessionExpiry, + device_identifier: device, + }) + + // 8. useCase - set session token cookie with the appropriate name and security settings + this.cookieRepository.setCookie({ + name: 'authjs.session-token', + value: sessionToken, + expires: sessionExpiry, + }) - await this.sessionRepository.createSession({ - sessionToken, - userId: user.id, - expires: sessionExpiry, - device_identifier: device, - }) - - // 7. useCase - set session token cookie with the appropriate name and security settings - this.cookieRepository.setCookie({ - name: 'authjs.session-token', - value: sessionToken, - expires: sessionExpiry, - }) - - return { - status: 201, - message: 'Usuário logado com sucesso!', - userId: user.id, + return { + status: 201, + message: 'Usuário logado com sucesso!', + userId: user.id, + } } } } diff --git a/src/use-cases/auth/login/credential/make-login-credential.ts b/src/use-cases/auth/login/credential/make-login-credential.ts index e0b2f42..9cf194c 100644 --- a/src/use-cases/auth/login/credential/make-login-credential.ts +++ b/src/use-cases/auth/login/credential/make-login-credential.ts @@ -1,16 +1,19 @@ import { NextCookieRepository } from '@/repositories/nextjs/next-cookie-repository' import { PgSessionRepository } from '@/repositories/pg/pg-session-repository' import { PgUserRepository } from '@/repositories/pg/pg-user-repository' +import { PgVerificationTokenRepository } from '@/repositories/pg/pg-verification-token-repository' import { LoginCredentialUseCase } from './login-credential' export function makeLoginCredentialUseCase() { const userRepository = new PgUserRepository() const sessionRepository = new PgSessionRepository() + const verificationTokenRepository = new PgVerificationTokenRepository() const cookieRepository = new NextCookieRepository() const useCase = new LoginCredentialUseCase( userRepository, sessionRepository, + verificationTokenRepository, cookieRepository, ) diff --git a/src/use-cases/auth/register/get-register.ts b/src/use-cases/auth/register/get-register.ts index de3c43c..91427b6 100644 --- a/src/use-cases/auth/register/get-register.ts +++ b/src/use-cases/auth/register/get-register.ts @@ -12,6 +12,7 @@ interface GetRegisterUseCaseResponse { email: string name?: string emailVerified?: string + two_factor_enabled?: boolean } } @@ -39,6 +40,7 @@ export class GetRegisterUseCase { email: userAlreadyExists.email, name: userAlreadyExists.nick_name || userAlreadyExists.name, emailVerified: userAlreadyExists.emailVerified, + two_factor_enabled: userAlreadyExists.two_factor_enabled, }, } }