AuthController.java
package com.edtech.controller;
import com.edtech.dto.AuthResponseDto;
import com.edtech.dto.LoginRequestDto;
import com.edtech.dto.RecoveryRequestDto;
import com.edtech.dto.RegisterRequestDto;
import com.edtech.dto.ResetPasswordDto;
import com.edtech.dto.UserResponseDto;
import com.edtech.dto.VerifyCodeDto;
import com.edtech.exception.RateLimitExceededException;
import com.edtech.model.User;
import com.edtech.security.RateLimitingService;
import com.edtech.service.JwtService;
import com.edtech.service.RecoveryService;
import com.edtech.service.TwoFactorAuthService;
import com.edtech.service.UserService;
import io.github.bucket4j.Bucket;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.validation.Valid;
import java.util.Map;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.security.core.Authentication;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
/** Documentação para AuthController. */
@RestController
@RequestMapping("/api/auth")
public class AuthController {
private final UserService userService;
private final JwtService jwtService;
private final RecoveryService recoveryService;
private final RateLimitingService rateLimitingService;
private final TwoFactorAuthService twoFactorAuthService;
/** Documentação. */
public AuthController(
UserService userService,
JwtService jwtService,
RecoveryService recoveryService,
RateLimitingService rateLimitingService,
TwoFactorAuthService twoFactorAuthService) {
this.userService = userService;
this.jwtService = jwtService;
this.recoveryService = recoveryService;
this.rateLimitingService = rateLimitingService;
this.twoFactorAuthService = twoFactorAuthService;
}
/** Documentação. */
@PostMapping("/register")
public ResponseEntity<Void> register(@Valid @RequestBody RegisterRequestDto request) {
userService.register(request);
return ResponseEntity.status(HttpStatus.CREATED).build();
}
/** Documentação. */
@PostMapping("/register/verify")
public ResponseEntity<AuthResponseDto> verifyRegistration(@RequestBody VerifyCodeDto request) {
User user = userService.verifyRegistration(request.email(), request.code());
String token = jwtService.generateToken(user);
return ResponseEntity.ok(new AuthResponseDto(UserResponseDto.from(user), token));
}
/** Documentação. */
@PostMapping("/login")
public ResponseEntity<?> login(
@Valid @RequestBody LoginRequestDto request, HttpServletRequest httpRequest) {
Bucket bucket = rateLimitingService.resolveBucket(httpRequest.getRemoteAddr());
if (!bucket.tryConsume(1)) {
throw new RateLimitExceededException(
"Limite de tentativas excedido. Tente novamente mais tarde.");
}
User user = userService.authenticate(request.email(), request.password());
if (user.isMfaEnabled()) {
return ResponseEntity.status(HttpStatus.ACCEPTED)
.body(Map.of("mfaRequired", true, "email", user.getEmail()));
}
String token = jwtService.generateToken(user);
return ResponseEntity.ok(new AuthResponseDto(UserResponseDto.from(user), token));
}
/** Javadoc. */
@PostMapping("/login/verify-2fa")
public ResponseEntity<?> verify2FaLogin(
@Valid @RequestBody com.edtech.dto.Verify2FaLoginDto request,
HttpServletRequest httpRequest) {
Bucket bucket = rateLimitingService.resolveBucket(httpRequest.getRemoteAddr());
if (!bucket.tryConsume(1)) {
throw new RateLimitExceededException(
"Limite de tentativas excedido. Tente novamente mais tarde.");
}
User user = userService.authenticate(request.email(), request.password());
if (!user.isMfaEnabled()) {
return ResponseEntity.badRequest().body("2FA is not enabled for this user.");
}
boolean isValid = twoFactorAuthService.verifyCode(user.getMfaSecret(), request.code());
if (!isValid) {
return ResponseEntity.status(HttpStatus.UNAUTHORIZED).body("Invalid 2FA code.");
}
String token = jwtService.generateToken(user);
return ResponseEntity.ok(new AuthResponseDto(UserResponseDto.from(user), token));
}
/** Javadoc. */
@GetMapping("/2fa/setup")
public ResponseEntity<?> setup2Fa(Authentication authentication) {
User user = (User) authentication.getPrincipal();
if (user.isMfaEnabled()) {
return ResponseEntity.badRequest().body("2FA is already enabled.");
}
String secret = user.getMfaSecret();
if (secret == null || secret.isEmpty()) {
secret = twoFactorAuthService.generateSecret();
user.setMfaSecret(secret);
userService.saveUserWithoutHash(
user); // Wait, we need a method to save the user without rehashing
}
try {
String qrCodeUri = twoFactorAuthService.getQrCodeImageUri(secret, user.getEmail());
return ResponseEntity.ok(Map.of("secret", secret, "qrCodeUri", qrCodeUri));
} catch (Exception e) {
return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR)
.body("Failed to generate QR Code");
}
}
/** Javadoc. */
@PostMapping("/2fa/enable")
public ResponseEntity<?> enable2Fa(
@RequestBody VerifyCodeDto request, Authentication authentication) {
User user = (User) authentication.getPrincipal();
if (user.isMfaEnabled()) {
return ResponseEntity.badRequest().body("2FA is already enabled.");
}
boolean isValid = twoFactorAuthService.verifyCode(user.getMfaSecret(), request.code());
if (!isValid) {
return ResponseEntity.badRequest().body("Invalid 2FA code.");
}
user.setMfaEnabled(true);
userService.saveUserWithoutHash(user);
return ResponseEntity.ok().build();
}
/** Documentação. */
@PostMapping("/recovery/request")
public ResponseEntity<?> requestRecovery(
@RequestBody RecoveryRequestDto request, HttpServletRequest httpRequest) {
Bucket bucket = rateLimitingService.resolveBucket(httpRequest.getRemoteAddr());
if (!bucket.tryConsume(1)) {
throw new RateLimitExceededException(
"Limite de tentativas excedido. Tente novamente mais tarde.");
}
recoveryService.requestRecovery(request.email());
return ResponseEntity.ok().build();
}
/** Documentação. */
@PostMapping("/recovery/verify")
public ResponseEntity<?> verifyCode(@RequestBody VerifyCodeDto request) {
boolean valid = recoveryService.verifyCode(request.email(), request.code());
if (valid) {
return ResponseEntity.ok().build();
}
return ResponseEntity.badRequest().body("Código inválido ou expirado.");
}
/** Documentação. */
@PostMapping("/recovery/reset")
public ResponseEntity<?> resetPassword(@RequestBody ResetPasswordDto request) {
boolean success =
recoveryService.resetPassword(request.email(), request.code(), request.newPassword());
if (success) {
return ResponseEntity.ok().build();
}
return ResponseEntity.badRequest().body("Erro ao redefinir a senha.");
}
/** Documentação. */
@PostMapping("/logout")
public ResponseEntity<Void> logout() {
return ResponseEntity.ok().build();
}
/** Documentação. */
@GetMapping("/me")
public ResponseEntity<UserResponseDto> me(Authentication authentication) {
User user = (User) authentication.getPrincipal();
return ResponseEntity.ok(UserResponseDto.from(user));
}
}