recovery del pass hecho en el backend a falta de hacer los formularios

This commit is contained in:
2025-09-28 18:36:44 +02:00
parent 22198b4f25
commit 865b1573b9
15 changed files with 517 additions and 8 deletions

View File

@ -0,0 +1,133 @@
package com.imprimelibros.erp.auth;
import com.imprimelibros.erp.common.RateLimiterService;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.validation.constraints.NotBlank;
import org.springframework.context.MessageSource;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.validation.annotation.Validated;
import org.springframework.web.bind.annotation.*;
import com.imprimelibros.erp.common.EmailService;
import com.imprimelibros.erp.users.UserDao;
import java.util.Locale;
import java.util.Map;
@Controller
@RequestMapping("/password")
@Validated
public class PasswordResetController {
private final PasswordResetService resetService;
private final EmailService emailService; // tu servicio de correo HTML
private final UserDao userRepo; // tu repo de usuarios
private final RateLimiterService rateLimiter;
private final MessageSource messageSource;
public PasswordResetController(PasswordResetService resetService,
EmailService emailService,
UserDao userRepo,
RateLimiterService rateLimiter,
MessageSource messageSource) {
this.resetService = resetService;
this.emailService = emailService;
this.userRepo = userRepo;
this.rateLimiter = rateLimiter;
this.messageSource = messageSource;
}
/** Endpoint para solicitar el email de reseteo (respuesta neutra y rate limiting) */
@PostMapping("/request")
@ResponseBody
public ResponseEntity<?> requestReset(@RequestParam("email") String email, HttpServletRequest req, Locale locale) {
String clientIp = extractClientIp(req);
// RATE LIMIT: 5/15min por IP -> si se supera, 429 con mensaje neutro
if (!rateLimiter.tryConsume("reset:" + clientIp)) {
// No revelamos nada concreto
return ResponseEntity.status(HttpStatus.TOO_MANY_REQUESTS)
.body(Map.of("message",
messageSource.getMessage("auth.reset.request.success", null, locale)));
}
userRepo.findByUserNameIgnoreCase(email).ifPresent(user -> {
String token = resetService.createTokenForUser(
user.getId(),
1, // caduca en 1h
clientIp,
req.getHeader("User-Agent")
);
String resetLink = buildResetLink(req, token);
try {
emailService.sendPasswordResetMail(user.getUserName(), user.getFullName(), resetLink, locale);
} catch (Exception ignored) {
// Deliberadamente no variamos la respuesta para no filtrar info
}
});
// Respuesta neutra SIEMPRE en 200 (si no fue 429)
return ResponseEntity.ok(Map.of("message",
messageSource.getMessage("auth.reset.request.success", null, locale)));
}
/** Muestra formulario si el token es válido */
@GetMapping("/reset")
public String showResetForm(@RequestParam("token") String token, Model model) {
Long userId = resetService.validateTokenAndGetUserId(token);
if (userId == null) {
model.addAttribute("invalidToken", true);
return "auth/reset-invalid";
}
model.addAttribute("token", token);
return "auth/reset-form";
}
/** Procesa y guarda nueva contraseña */
@PostMapping("/reset")
public String handleReset(
@RequestParam("token") String token,
@RequestParam("password") @NotBlank String password,
@RequestParam("confirmPassword") @NotBlank String confirmPassword,
Model model, Locale locale
) {
if (!password.equals(confirmPassword)) {
model.addAttribute("token", token);
model.addAttribute("error", messageSource.getMessage("auth.reset.form.passwordsMismatch", null, locale));
return "auth/reset-form";
}
try {
resetService.consumeTokenAndSetPassword(token, password);
return "redirect:/login?resetOk";
} catch (IllegalArgumentException ex) {
model.addAttribute("invalidToken", true);
return "auth/reset-invalid";
}
}
private static String extractClientIp(HttpServletRequest req) {
// Soporte detrás de proxy
String xff = req.getHeader("X-Forwarded-For");
if (xff != null && !xff.isBlank()) {
// coger el primer IP de la cadena
return xff.split(",")[0].trim();
}
String realIp = req.getHeader("X-Real-IP");
if (realIp != null && !realIp.isBlank()) return realIp.trim();
return req.getRemoteAddr();
}
private static String buildResetLink(HttpServletRequest req, String token) {
String scheme = req.getHeader("X-Forwarded-Proto");
if (scheme == null || scheme.isBlank()) scheme = req.getScheme(); // http/https
String host = req.getHeader("Host"); // respeta el host público bajo proxy
if (host == null || host.isBlank()) host = req.getServerName() + (req.getServerPort() != 80 && req.getServerPort() != 443 ? ":" + req.getServerPort() : "");
String ctx = req.getContextPath() == null ? "" : req.getContextPath();
return scheme + "://" + host + ctx + "/password/reset?token=" + token;
}
}