password forgot hecho

This commit is contained in:
2025-10-04 16:51:22 +02:00
parent b66ceee85c
commit dbc2038f9f
14 changed files with 366 additions and 248 deletions

View File

@ -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);
}
}