mirror of
https://git.imnavajas.es/jjimenez/erp-imprimelibros.git
synced 2026-02-01 00:28:51 +00:00
recovery del pass hecho en el backend a falta de hacer los formularios
This commit is contained in:
@ -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;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user