mirror of
https://git.imnavajas.es/jjimenez/erp-imprimelibros.git
synced 2026-01-13 08:58:48 +00:00
password forgot hecho
This commit is contained in:
@ -1,134 +1,92 @@
|
||||
package com.imprimelibros.erp.auth;
|
||||
|
||||
import com.imprimelibros.erp.common.RateLimiterService;
|
||||
import com.imprimelibros.erp.common.email.EmailService;
|
||||
|
||||
import jakarta.servlet.http.HttpServletRequest;
|
||||
import jakarta.validation.constraints.Email;
|
||||
import jakarta.validation.constraints.NotBlank;
|
||||
|
||||
import java.util.Locale;
|
||||
|
||||
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 org.springframework.web.servlet.mvc.support.RedirectAttributes;
|
||||
|
||||
import com.imprimelibros.erp.users.UserDao;
|
||||
|
||||
import java.util.Locale;
|
||||
import java.util.Map;
|
||||
import jakarta.servlet.http.HttpServletRequest;
|
||||
|
||||
@Controller
|
||||
@RequestMapping("/password")
|
||||
@RequestMapping("/auth/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 PasswordResetService service;
|
||||
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;
|
||||
public PasswordResetController(PasswordResetService service, MessageSource messageSource) {
|
||||
this.service = service;
|
||||
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)));
|
||||
// 4.1 Página "¿Olvidaste tu contraseña?"
|
||||
@GetMapping("/forgot")
|
||||
public String forgotForm(Model model, Locale locale) {
|
||||
model.addAttribute("form", "_forgot-password");
|
||||
return "imprimelibros/login/login";
|
||||
}
|
||||
|
||||
/** Muestra formulario si el token es válido */
|
||||
// 4.2 Envío del email (si existe)
|
||||
@PostMapping("/forgot")
|
||||
public String handleForgot(
|
||||
@RequestParam @NotBlank @Email String username,
|
||||
HttpServletRequest request,
|
||||
Model model,
|
||||
RedirectAttributes ra,
|
||||
Locale locale) {
|
||||
String baseUrl = request.getScheme() + "://" + request.getServerName()
|
||||
+ (request.getServerPort() == 80 || request.getServerPort() == 443 ? ""
|
||||
: ":" + request.getServerPort());
|
||||
String ip = request.getRemoteAddr();
|
||||
String ua = request.getHeader("User-Agent");
|
||||
service.requestReset(username, baseUrl, ip, ua, 60, locale);
|
||||
|
||||
ra.addFlashAttribute("info", messageSource.getMessage("login.password-recovery.email-info", null, locale));
|
||||
return "redirect:/login";
|
||||
}
|
||||
|
||||
// 4.3 Formulario de nueva contraseña (a partir del enlace)
|
||||
@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(
|
||||
public String resetForm(@RequestParam("uid") Long uid,
|
||||
@RequestParam("token") String token,
|
||||
@RequestParam("password") @NotBlank String password,
|
||||
@RequestParam("confirmPassword") @NotBlank String confirmPassword,
|
||||
Model model, Locale locale
|
||||
) {
|
||||
if (!password.equals(confirmPassword)) {
|
||||
Model model, Locale locale) {
|
||||
boolean ok = service.isValid(uid, token);
|
||||
model.addAttribute("uid", uid);
|
||||
model.addAttribute("token", token);
|
||||
model.addAttribute("valid", ok);
|
||||
|
||||
model.addAttribute("form", "_reset-password");
|
||||
return "imprimelibros/login/login";
|
||||
}
|
||||
|
||||
// 4.4 Confirmación del reseteo
|
||||
@PostMapping("/reset")
|
||||
public String doReset(@RequestParam("uid") Long uid,
|
||||
@RequestParam("token") String token,
|
||||
@RequestParam("password") String password,
|
||||
@RequestParam("password2") String password2,
|
||||
Model model, Locale locale) {
|
||||
if (!password.equals(password2) || password.length() < 8) {
|
||||
model.addAttribute("uid", uid);
|
||||
model.addAttribute("token", token);
|
||||
model.addAttribute("error", messageSource.getMessage("auth.reset.form.passwordsMismatch", null, locale));
|
||||
return "auth/reset-form";
|
||||
model.addAttribute("danger", messageSource.getMessage("login.password-reset.error", null, locale));
|
||||
model.addAttribute("form", "_reset-password");
|
||||
return "imprimelibros/login/login";
|
||||
}
|
||||
|
||||
try {
|
||||
resetService.consumeTokenAndSetPassword(token, password);
|
||||
return "redirect:/login?resetOk";
|
||||
} catch (IllegalArgumentException ex) {
|
||||
model.addAttribute("invalidToken", true);
|
||||
return "auth/reset-invalid";
|
||||
if (service.resetPassword(uid, token, password)) {
|
||||
model.addAttribute("info", messageSource.getMessage("login.password-reset.success", null, locale));
|
||||
} else {
|
||||
model.addAttribute("danger", messageSource.getMessage("login.password-reset.error-link", null, locale));
|
||||
}
|
||||
}
|
||||
|
||||
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;
|
||||
model.addAttribute("form", "_login");
|
||||
return "imprimelibros/login/login";
|
||||
}
|
||||
}
|
||||
|
||||
@ -1,86 +1,124 @@
|
||||
package com.imprimelibros.erp.auth;
|
||||
|
||||
import org.springframework.security.crypto.password.PasswordEncoder;
|
||||
import org.springframework.stereotype.Service;
|
||||
|
||||
import com.imprimelibros.erp.users.UserDao;
|
||||
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.security.MessageDigest;
|
||||
import java.security.SecureRandom;
|
||||
import java.time.Instant;
|
||||
import java.time.temporal.ChronoUnit;
|
||||
import java.util.HexFormat;
|
||||
import java.time.LocalDateTime;
|
||||
import java.util.Base64;
|
||||
import java.util.Locale;
|
||||
import java.util.Map;
|
||||
|
||||
import org.springframework.context.MessageSource;
|
||||
import org.springframework.mail.javamail.JavaMailSender;
|
||||
import org.springframework.security.crypto.password.PasswordEncoder;
|
||||
import org.springframework.stereotype.Service;
|
||||
import org.springframework.transaction.annotation.Transactional;
|
||||
import org.thymeleaf.context.Context;
|
||||
import org.thymeleaf.spring6.SpringTemplateEngine;
|
||||
|
||||
import com.imprimelibros.erp.common.email.EmailService;
|
||||
import com.imprimelibros.erp.users.User;
|
||||
import com.imprimelibros.erp.users.UserDao; // ajusta al path real de tu UserRepository
|
||||
|
||||
@Service
|
||||
public class PasswordResetService {
|
||||
|
||||
private final PasswordResetTokenRepository tokenRepo;
|
||||
private final UserDao userRepo; // tu repo real
|
||||
private final UserDao userRepo;
|
||||
private final PasswordEncoder passwordEncoder;
|
||||
private final JavaMailSender mailSender;
|
||||
private final SpringTemplateEngine templateEngine;
|
||||
private final MessageSource messages;
|
||||
private final EmailService emailService;
|
||||
|
||||
private static final SecureRandom RNG = new SecureRandom();
|
||||
private static final HexFormat HEX = HexFormat.of();
|
||||
|
||||
public PasswordResetService(PasswordResetTokenRepository tokenRepo, UserDao userRepo, PasswordEncoder enc) {
|
||||
public PasswordResetService(
|
||||
PasswordResetTokenRepository tokenRepo,
|
||||
UserDao userRepo,
|
||||
PasswordEncoder passwordEncoder,
|
||||
JavaMailSender mailSender,
|
||||
SpringTemplateEngine templateEngine,
|
||||
MessageSource messages,
|
||||
EmailService emailService
|
||||
) {
|
||||
this.tokenRepo = tokenRepo;
|
||||
this.userRepo = userRepo;
|
||||
this.passwordEncoder = enc;
|
||||
this.passwordEncoder = passwordEncoder;
|
||||
this.mailSender = mailSender;
|
||||
this.templateEngine = templateEngine;
|
||||
this.messages = messages;
|
||||
this.emailService = emailService;
|
||||
}
|
||||
|
||||
/** Elimina tokens previos sin usar, genera uno nuevo y lo guarda con auditoría básica. */
|
||||
public String createTokenForUser(Long userId, int hoursToExpire, String requestIp, String userAgent) {
|
||||
// Invalidar anteriores
|
||||
tokenRepo.deleteAllByUserIdAndUsedAtIsNull(userId);
|
||||
// 3.1 Solicitar reset (si el email existe, genera token y envía)
|
||||
@Transactional
|
||||
public void requestReset(String email, String baseUrl, String ip, String userAgent, int minutes, Locale locale) {
|
||||
|
||||
User user = userRepo.findByUserNameIgnoreCase(email).orElse(null);
|
||||
// Siempre responder OK aunque no exista para evitar enumeración
|
||||
if (user == null) return;
|
||||
|
||||
// Generar token
|
||||
byte[] raw = new byte[32];
|
||||
RNG.nextBytes(raw);
|
||||
String token = HEX.formatHex(raw); // token plano (64 hex)
|
||||
String tokenHash = sha256(token);
|
||||
tokenRepo.invalidateActiveTokens(user.getId(), LocalDateTime.now());
|
||||
|
||||
PasswordResetToken prt = new PasswordResetToken();
|
||||
prt.setUserId(userId);
|
||||
prt.setTokenHash(tokenHash);
|
||||
prt.setExpiresAt(Instant.now().plus(hoursToExpire, ChronoUnit.HOURS));
|
||||
prt.setRequestIp(truncate(requestIp, 64));
|
||||
prt.setUserAgent(truncate(userAgent, 255));
|
||||
tokenRepo.save(prt);
|
||||
String token = generateToken(); // token en claro SOLO para el enlace
|
||||
String tokenHash = sha256(token); // guardamos hash en DB
|
||||
|
||||
return token; // Esto se envía por email
|
||||
PasswordResetToken row = new PasswordResetToken();
|
||||
row.setUserId(user.getId());
|
||||
row.setCreatedAt(LocalDateTime.now());
|
||||
row.setExpiresAt(LocalDateTime.now().plusMinutes(minutes));
|
||||
row.setRequestIp(ip);
|
||||
row.setUserAgent(userAgent);
|
||||
row.setTokenHash(tokenHash);
|
||||
tokenRepo.save(row);
|
||||
|
||||
String resetUrl = baseUrl + "/auth/password/reset?uid=" + user.getId() + "&token=" + token;
|
||||
|
||||
emailService.sendPasswordResetMail(user.getUserName(), user.getFullName(), resetUrl, locale);
|
||||
}
|
||||
|
||||
public Long validateTokenAndGetUserId(String tokenPlain) {
|
||||
return tokenRepo.findByTokenHashAndUsedAtIsNullAndExpiresAtAfter(sha256(tokenPlain), Instant.now())
|
||||
.map(PasswordResetToken::getUserId)
|
||||
.orElse(null);
|
||||
// 3.2 Validar token (para mostrar el formulario de nueva contraseña)
|
||||
public boolean isValid(Long userId, String token) {
|
||||
String hash = sha256(token);
|
||||
return tokenRepo.findValidByUserAndHash(userId, hash, LocalDateTime.now()).isPresent();
|
||||
}
|
||||
|
||||
public void consumeTokenAndSetPassword(String tokenPlain, String newPassword) {
|
||||
var tokenOpt = tokenRepo.findByTokenHashAndUsedAtIsNullAndExpiresAtAfter(sha256(tokenPlain), Instant.now());
|
||||
var prt = tokenOpt.orElseThrow(() -> new IllegalArgumentException("Token inválido o caducado"));
|
||||
// 3.3 Confirmar reseteo y marcar token como usado
|
||||
@Transactional
|
||||
public boolean resetPassword(Long userId, String token, String newPassword) {
|
||||
String hash = sha256(token);
|
||||
var opt = tokenRepo.findValidByUserAndHash(userId, hash, LocalDateTime.now());
|
||||
if (opt.isEmpty()) return false;
|
||||
|
||||
var user = userRepo.findById(prt.getUserId())
|
||||
.orElseThrow(() -> new IllegalStateException("Usuario no encontrado"));
|
||||
var entry = opt.get();
|
||||
var user = userRepo.findById(userId).orElse(null);
|
||||
if (user == null) return false;
|
||||
|
||||
user.setPassword(passwordEncoder.encode(newPassword));
|
||||
userRepo.save(user);
|
||||
|
||||
prt.setUsedAt(Instant.now());
|
||||
tokenRepo.save(prt);
|
||||
entry.setUsedAt(LocalDateTime.now());
|
||||
tokenRepo.save(entry);
|
||||
|
||||
// (Opcional) invalidar otros tokens activos del usuario
|
||||
tokenRepo.invalidateActiveTokens(userId, LocalDateTime.now());
|
||||
return true;
|
||||
}
|
||||
|
||||
private static String sha256(String s) {
|
||||
// --- helpers ---
|
||||
private String generateToken() {
|
||||
byte[] buf = new byte[32];
|
||||
new SecureRandom().nextBytes(buf);
|
||||
return Base64.getUrlEncoder().withoutPadding().encodeToString(buf);
|
||||
}
|
||||
private String sha256(String s) {
|
||||
try {
|
||||
MessageDigest md = MessageDigest.getInstance("SHA-256");
|
||||
return HEX.formatHex(md.digest(s.getBytes(StandardCharsets.UTF_8)));
|
||||
var md = MessageDigest.getInstance("SHA-256");
|
||||
return Base64.getEncoder().encodeToString(md.digest(s.getBytes(StandardCharsets.UTF_8)));
|
||||
} catch (Exception e) {
|
||||
throw new RuntimeException(e);
|
||||
}
|
||||
}
|
||||
|
||||
private static String truncate(String v, int max) {
|
||||
if (v == null) return null;
|
||||
return v.length() <= max ? v : v.substring(0, max);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
||||
@ -1,38 +1,36 @@
|
||||
package com.imprimelibros.erp.auth;
|
||||
|
||||
import java.time.LocalDateTime;
|
||||
import jakarta.persistence.*;
|
||||
import java.time.Instant;
|
||||
|
||||
@Entity
|
||||
@Table(name = "password_reset_tokens", indexes = {
|
||||
@Index(name = "idx_prt_token_hash", columnList = "tokenHash"),
|
||||
@Index(name = "idx_prt_user_id", columnList = "userId")
|
||||
})
|
||||
@Table(name = "password_reset_tokens")
|
||||
public class PasswordResetToken {
|
||||
|
||||
@Id @GeneratedValue(strategy = GenerationType.IDENTITY)
|
||||
private Long id;
|
||||
|
||||
@Column(nullable = false) private Long userId;
|
||||
@Column(name = "created_at", nullable = false)
|
||||
private LocalDateTime createdAt;
|
||||
|
||||
@Column(nullable = false, length = 128)
|
||||
private String tokenHash; // SHA-256 hex
|
||||
@Column(name = "expires_at", nullable = false)
|
||||
private LocalDateTime expiresAt;
|
||||
|
||||
@Column(nullable = false)
|
||||
private Instant expiresAt;
|
||||
|
||||
@Column(nullable = false)
|
||||
private Instant createdAt = Instant.now();
|
||||
|
||||
private Instant usedAt;
|
||||
|
||||
// Auditoría ligera (GDPR: documéntalo y limita retención)
|
||||
@Column(length = 64)
|
||||
@Column(name = "request_ip", length = 64)
|
||||
private String requestIp;
|
||||
|
||||
@Column(length = 255)
|
||||
@Column(name = "token_hash", length = 128, nullable = false)
|
||||
private String tokenHash;
|
||||
|
||||
@Column(name = "used_at")
|
||||
private LocalDateTime usedAt;
|
||||
|
||||
@Column(name = "user_agent", length = 255)
|
||||
private String userAgent;
|
||||
|
||||
@Column(name = "user_id", nullable = false)
|
||||
private Long userId;
|
||||
|
||||
public Long getId() {
|
||||
return id;
|
||||
}
|
||||
@ -41,44 +39,20 @@ public class PasswordResetToken {
|
||||
this.id = id;
|
||||
}
|
||||
|
||||
public Long getUserId() {
|
||||
return userId;
|
||||
}
|
||||
|
||||
public void setUserId(Long userId) {
|
||||
this.userId = userId;
|
||||
}
|
||||
|
||||
public String getTokenHash() {
|
||||
return tokenHash;
|
||||
}
|
||||
|
||||
public void setTokenHash(String tokenHash) {
|
||||
this.tokenHash = tokenHash;
|
||||
}
|
||||
|
||||
public Instant getExpiresAt() {
|
||||
return expiresAt;
|
||||
}
|
||||
|
||||
public void setExpiresAt(Instant expiresAt) {
|
||||
this.expiresAt = expiresAt;
|
||||
}
|
||||
|
||||
public Instant getCreatedAt() {
|
||||
public LocalDateTime getCreatedAt() {
|
||||
return createdAt;
|
||||
}
|
||||
|
||||
public void setCreatedAt(Instant createdAt) {
|
||||
public void setCreatedAt(LocalDateTime createdAt) {
|
||||
this.createdAt = createdAt;
|
||||
}
|
||||
|
||||
public Instant getUsedAt() {
|
||||
return usedAt;
|
||||
public LocalDateTime getExpiresAt() {
|
||||
return expiresAt;
|
||||
}
|
||||
|
||||
public void setUsedAt(Instant usedAt) {
|
||||
this.usedAt = usedAt;
|
||||
public void setExpiresAt(LocalDateTime expiresAt) {
|
||||
this.expiresAt = expiresAt;
|
||||
}
|
||||
|
||||
public String getRequestIp() {
|
||||
@ -89,6 +63,22 @@ public class PasswordResetToken {
|
||||
this.requestIp = requestIp;
|
||||
}
|
||||
|
||||
public String getTokenHash() {
|
||||
return tokenHash;
|
||||
}
|
||||
|
||||
public void setTokenHash(String tokenHash) {
|
||||
this.tokenHash = tokenHash;
|
||||
}
|
||||
|
||||
public LocalDateTime getUsedAt() {
|
||||
return usedAt;
|
||||
}
|
||||
|
||||
public void setUsedAt(LocalDateTime usedAt) {
|
||||
this.usedAt = usedAt;
|
||||
}
|
||||
|
||||
public String getUserAgent() {
|
||||
return userAgent;
|
||||
}
|
||||
@ -97,6 +87,13 @@ public class PasswordResetToken {
|
||||
this.userAgent = userAgent;
|
||||
}
|
||||
|
||||
// getters/setters
|
||||
public Long getUserId() {
|
||||
return userId;
|
||||
}
|
||||
|
||||
public void setUserId(Long userId) {
|
||||
this.userId = userId;
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
}
|
||||
@ -1,13 +1,40 @@
|
||||
package com.imprimelibros.erp.auth;
|
||||
|
||||
import org.springframework.data.jpa.repository.JpaRepository;
|
||||
import java.time.Instant;
|
||||
import java.time.LocalDateTime;
|
||||
import java.util.Optional;
|
||||
import org.springframework.data.jpa.repository.*;
|
||||
import java.util.List;
|
||||
|
||||
public interface PasswordResetTokenRepository extends JpaRepository<PasswordResetToken, Long> {
|
||||
|
||||
Optional<PasswordResetToken> findByTokenHashAndUsedAtIsNullAndExpiresAtAfter(String tokenHash, Instant now);
|
||||
// Para validar un token en el reset
|
||||
@Query("""
|
||||
SELECT t FROM PasswordResetToken t
|
||||
WHERE t.userId = :userId
|
||||
AND t.tokenHash = :tokenHash
|
||||
AND t.usedAt IS NULL
|
||||
AND t.expiresAt > :now
|
||||
""")
|
||||
Optional<PasswordResetToken> findValidByUserAndHash(Long userId, String tokenHash, LocalDateTime now);
|
||||
|
||||
// Para invalidar anteriores
|
||||
long deleteAllByUserIdAndUsedAtIsNull(Long userId);
|
||||
// Invalida cualquier token activo del usuario
|
||||
@Modifying
|
||||
@Query("""
|
||||
UPDATE PasswordResetToken t
|
||||
SET t.usedAt = :now
|
||||
WHERE t.userId = :userId
|
||||
AND t.usedAt IS NULL
|
||||
AND t.expiresAt > :now
|
||||
""")
|
||||
int invalidateActiveTokens(Long userId, LocalDateTime now);
|
||||
|
||||
// (Opcional) lista de activos, por si quieres inspeccionarlos
|
||||
@Query("""
|
||||
SELECT t FROM PasswordResetToken t
|
||||
WHERE t.userId = :userId
|
||||
AND t.usedAt IS NULL
|
||||
AND t.expiresAt > :now
|
||||
ORDER BY t.createdAt DESC
|
||||
""")
|
||||
List<PasswordResetToken> findAllActiveForUser(Long userId, LocalDateTime now);
|
||||
}
|
||||
|
||||
@ -7,7 +7,6 @@ import org.springframework.context.MessageSource;
|
||||
import org.springframework.mail.javamail.JavaMailSender;
|
||||
import org.springframework.mail.javamail.MimeMessageHelper;
|
||||
import org.springframework.stereotype.Service;
|
||||
import org.springframework.web.servlet.support.ServletUriComponentsBuilder;
|
||||
import org.thymeleaf.TemplateEngine;
|
||||
import org.thymeleaf.context.Context;
|
||||
import org.springframework.core.io.ClassPathResource;
|
||||
@ -29,11 +28,12 @@ public class EmailService {
|
||||
}
|
||||
|
||||
public void sendPasswordResetMail(String to, String fullName, String resetUrl, Locale locale) {
|
||||
String subject = messageSource.getMessage("email.resetPassword.title", null, locale);
|
||||
String subject = messageSource.getMessage("email.reset-password.title", null, locale);
|
||||
Map<String, Object> variables = Map.of(
|
||||
"fullName", fullName,
|
||||
"resetUrl", resetUrl);
|
||||
sendEmail(to, subject, "imprimelibros/email/reset-password", variables, locale);
|
||||
"resetUrl", resetUrl,
|
||||
"minutes", 60);
|
||||
sendEmail(to, subject, "imprimelibros/email/password-reset", variables, locale);
|
||||
}
|
||||
|
||||
public void sendVerificationEmail(String to, String fullName, String verifyUrl, Locale locale) {
|
||||
|
||||
@ -126,7 +126,7 @@ public class SecurityConfig {
|
||||
"/login",
|
||||
"/signup",
|
||||
"/verify",
|
||||
"/reset-password",
|
||||
"/auth/password/**",
|
||||
"/assets/**",
|
||||
"/css/**",
|
||||
"/js/**",
|
||||
|
||||
Reference in New Issue
Block a user