package com.imprimelibros.erp.auth; import java.nio.charset.StandardCharsets; import java.security.MessageDigest; import java.security.SecureRandom; 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; private final PasswordEncoder passwordEncoder; private final JavaMailSender mailSender; private final SpringTemplateEngine templateEngine; private final MessageSource messages; private final EmailService emailService; public PasswordResetService( PasswordResetTokenRepository tokenRepo, UserDao userRepo, PasswordEncoder passwordEncoder, JavaMailSender mailSender, SpringTemplateEngine templateEngine, MessageSource messages, EmailService emailService ) { this.tokenRepo = tokenRepo; this.userRepo = userRepo; this.passwordEncoder = passwordEncoder; this.mailSender = mailSender; this.templateEngine = templateEngine; this.messages = messages; this.emailService = emailService; } // 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; tokenRepo.invalidateActiveTokens(user.getId(), LocalDateTime.now()); String token = generateToken(); // token en claro SOLO para el enlace String tokenHash = sha256(token); // guardamos hash en DB 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); } // 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(); } // 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 entry = opt.get(); var user = userRepo.findById(userId).orElse(null); if (user == null) return false; user.setPassword(passwordEncoder.encode(newPassword)); userRepo.save(user); entry.setUsedAt(LocalDateTime.now()); tokenRepo.save(entry); // (Opcional) invalidar otros tokens activos del usuario tokenRepo.invalidateActiveTokens(userId, LocalDateTime.now()); return true; } // --- 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 { var md = MessageDigest.getInstance("SHA-256"); return Base64.getEncoder().encodeToString(md.digest(s.getBytes(StandardCharsets.UTF_8))); } catch (Exception e) { throw new RuntimeException(e); } } }