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,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);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user